Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f6249a2
fix: trying to prevent a deadlock
themarolt Jun 17, 2026
eafcb53
fix: using proper tables for nuget and go
themarolt Jun 17, 2026
9b77640
fix: flag to update constraints
themarolt Jun 17, 2026
9fd28f7
chore: improvements to monitoring
themarolt Jun 17, 2026
584cf90
chore: more states
themarolt Jun 17, 2026
c4e31ac
fix: use the same env as in deployment
themarolt Jun 17, 2026
ba96498
fix: better deploy script and monitor fix
themarolt Jun 17, 2026
954a751
fix: monitor column spacing fix
themarolt Jun 17, 2026
1f3fda0
fix: dont be done prematurely
themarolt Jun 17, 2026
fd18287
fix: index creation improvements
themarolt Jun 18, 2026
755a317
chore: script to fix unique index on packages_dependents
themarolt Jun 18, 2026
73ad88b
fix: better index creation
themarolt Jun 19, 2026
078ccae
Merge branch 'main' into fix/bq-depsdev-deadlock
themarolt Jun 19, 2026
aac2ccd
Merge remote-tracking branch 'origin/main' into fix/bq-depsdev-deadlock
themarolt Jun 19, 2026
0f33398
fix: comments
themarolt Jun 19, 2026
719ce84
Merge remote-tracking branch 'origin/main' into fix/bq-depsdev-deadlock
themarolt Jun 19, 2026
e2ca951
fix: comments
themarolt Jun 19, 2026
826b42e
Merge remote-tracking branch 'origin/main' into fix/bq-depsdev-deadlock
themarolt Jun 19, 2026
d369171
fix: comments
themarolt Jun 19, 2026
61b4b31
Merge remote-tracking branch 'origin/main' into fix/bq-depsdev-deadlock
themarolt Jun 19, 2026
f56818a
fix: comments
themarolt Jun 19, 2026
26f0680
fix: comments
themarolt Jun 19, 2026
8656ec6
fix: dedup deps.dev incremental edges, guard corrupt snapshots (CM-1281)
themarolt Jun 22, 2026
7d32df6
Merge remote-tracking branch 'origin/main' into fix/incremental-depsd…
themarolt Jun 24, 2026
1ca7b17
style: format deps-dev README (CM-1281)
themarolt Jun 25, 2026
16f5577
Merge remote-tracking branch 'origin/main' into fix/incremental-depsd…
themarolt Jun 25, 2026
07a2ec7
Merge remote-tracking branch 'origin/main' into fix/incremental-depsd…
themarolt Jun 26, 2026
a76d13e
refactor: clarify edge-snapshot guard threshold naming (CM-1281)
themarolt Jun 28, 2026
1c70251
Merge remote-tracking branch 'origin/main' into fix/incremental-depsd…
themarolt Jun 28, 2026
3b39fd3
fix: comments
themarolt Jun 28, 2026
4768874
fix: small cleanup
themarolt Jun 28, 2026
dee16ec
fix: comments
themarolt Jun 28, 2026
561766e
fix: comments
themarolt Jun 28, 2026
053aefe
fix: comments
themarolt Jun 28, 2026
c7a6dd0
fix: comments
themarolt Jun 29, 2026
0fdd301
fix: comments
themarolt Jun 29, 2026
c2ba05e
Merge remote-tracking branch 'origin/main' into fix/incremental-depsd…
themarolt Jun 29, 2026
4a7e5ca
fix: comments
themarolt Jun 29, 2026
ac2de8a
Merge remote-tracking branch 'origin/main' into fix/incremental-depsd…
themarolt Jun 29, 2026
f8c4463
fix: comments
themarolt Jun 29, 2026
7dd4205
Merge remote-tracking branch 'origin/main' into fix/incremental-depsd…
themarolt Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions docs/adr/0004-go-nuget-transitive-dependent-counts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# ADR-0004: Compute GO/NUGET transitive dependent counts via exact reverse closure (over HLL approximation)

**Date**: 2026-06-23
**Status**: accepted
**Deciders**: Uroš Marolt

## Context
deps.dev publishes its reverse-dependent index (`Dependents`) and its resolved/transitive
dependency graph (`Dependencies`, `DependencyGraphEdges`) for only four ecosystems —
NPM, MAVEN, PYPI, CARGO (verified via BQ 2026-06-23: each `GROUP BY System` returns exactly
those four). GO and NUGET have only raw direct manifests (`GoRequirementsLatest`,
`NuGetRequirementsLatest`). The `dependent_counts` ingest was therefore split three ways
(edges / go / nuget — see the dependent-counts split). Direct `dependent_count` for GO/NUGET
is a cheap manifest invert, but `transitive_dependent_count` (MinimumDepth>1) and the all-depth
`dependent_repos_count` require the reverse **transitive closure**, which deps.dev does not
provide for these two ecosystems — so we must compute it ourselves from the direct edges.

## Decision
Compute the **exact** reverse transitive closure for GO/NUGET — a semi-naive fixpoint over
module-level edges (names hashed to INT64 via `FARM_FINGERPRINT`, persisted + clustered) iterated
to convergence — so GO/NUGET transitive counts are true integers matching the edge-system
methodology. We chose exact over an HLL-sketch approximation, accepting the higher cost for now.

## Alternatives Considered

### Alternative 1: HLL sketch propagation (approximate)
- **Pros**: ~cents/run, seconds, tiny tables (one HyperLogLog sketch per subject, ~188K rows for
GO, merged along edges to a fixpoint — never materializes the billions of pairs). Full-depth.
- **Cons**: probabilistic cardinality estimate with bounded relative error ≈ `1.04/sqrt(2^p)`
(~1.6% at precision 12, ~0.4% at 15). Methodology differs from the exact edge-system counts, so
GO/NUGET would be ~1% approximate while the other four ecosystems are exact.
- **Why not**: the decision is to have identical, exact integer methodology across all six
ecosystems. The measured exact cost (below) is acceptable for now. HLL remains the documented
fallback if weekly cost becomes prohibitive.

### Alternative 2: Bounded-depth closure (cap at K hops)
- **Pros**: K deterministic joins in a single statement, dry-runnable, cost-bounded, no scripting.
- **Cons**: undercounts. The GO closure needs **31 hops** to converge; new pairs peak at hop 8
(~507M) and only decay to zero by hop 31. Capping at a small K drops hundreds of millions of
genuine transitive pairs.
- **Why not**: a low cap is materially wrong; a cap high enough to be correct is no cheaper than
full convergence.

## Consequences

### Positive
- Exact integer counts, full depth, identical methodology to NPM/MAVEN/PYPI/CARGO.
- Deterministic and reproducible; no estimator error to explain to downstream consumers.
- Termination is guaranteed by construction: each iteration's frontier is anti-joined against the
accumulated `reach`, so only brand-new pairs survive and the finite pair space converges (proven:
GO converged at hop 31 with `new_pairs → 0`).

### Negative
- This is by far the heaviest job in the packages pipeline. Measured exact runs (2026-06-23,
INT64-fingerprint-optimized; raw string names would be ~5×). The build-`reach` figures are from the
closure probe; the full-pipeline figures (incl. the all-depth repos aggregation + per-subject
counts → `_export_data`) are from the end-to-end validation run of the production script:
- **GO**: 5.81B closure pairs, 31 iterations. Build reach ≈ 1.86 TB; **full pipeline = 2.31 TB
billed (~$14.5 at $6.25/TiB), 99 slot-hours, 16 min wall, 132 child statements** (validated).
- **NUGET**: 114M closure pairs, 19 iterations, ~4 min wall, ~32 GB billed (~$0.19); full pipeline
validated → 191,762 subject rows.
- GO dominates (its graph is ~50× larger and deeper). Combined ≈ **~$15/run, ~20 min, weekly**.
For comparison the edge `dependent_counts` job scans ~310 GB (~$2).
- Requires a semi-naive BQ scripting job (not a single exportable SELECT), so it does not fit the
existing one-query→GCS pipeline shape as-is (integrated via an `isScript` mode — see below).

### Risks
- Cost/runtime grow with the GO/NUGET graphs. Mitigations: INT64 fingerprints (~5× cheaper),
clustering on the join keys, and a documented escape hatch to switch GO/NUGET to HLL (Alternative
1) if cost becomes prohibitive — counts in the hundred-thousands make ~1% error invisible for
popularity/ranking.
- `FARM_FINGERPRINT` collisions are negligible at ~1–2M nodes (~1e-6) but non-zero; acceptable for
these metrics. Switch to a dictionary-encoded INT id if exactness must be collision-proof.

## Pipeline integration (decided 2026-06-23)
- **No persistent scratch dataset.** The closure runs as one multi-statement scripting job using
session-scoped `CREATE TEMP TABLE`s (edges → INT64-fingerprinted edges → `reach` fixpoint →
fingerprint→name lookup → `_export_data`). Temp tables are clustered on the join keys and are
auto-dropped when the script's session ends — on success *and* failure — so there is no lingering
storage to bill or clean up. (The interactive probe used regular tables in
`lfx-insights:scratch_closure_probe`; that is leftover, dropped separately — not the pipeline.)
- **Reuses the existing job path, not a new kind.** `bqExportToGcs` gains an `isScript` mode: when
set, `sql` is the full script (ending in `CREATE TEMP TABLE _export_data AS …`) and the activity
appends only the `EXPORT DATA … AS SELECT * FROM _export_data` statement rather than wrapping
`sql` in a subquery. Everything downstream (listParquetFiles → guard → chunked
gcsParquetToStaging → mergeStagingToTable) is unchanged, and the `dependent_counts_go` /
`dependent_counts_nuget` kinds, guard baselines, and monitor entries stay continuous.
- **Cost guard.** A dry-run cannot price a `WHILE` loop, so script mode skips it and enforces the
byte ceiling server-side via `maximumBytesBilled` (= the per-variant `maxBytesGb`). The script
also carries an iteration cap as the deterministic runaway guard (convergence already proven:
GO hop 31, NUGET hop 19, both `new_pairs → 0`).
- **Final output → counts.** `_export_data` emits `(purl, dependent_count,
transitive_dependent_count, dependent_repos_count)`: fingerprints in `reach` are mapped back to
names via a `names` temp table, joined to `purl_map` (name→purl). `transitive_dependent_count =
COUNT(DISTINCT dep over reach) − direct_count`; `dependent_repos_count` = distinct
`PackageVersionToProject` SOURCE_REPO_TYPE repos over the all-depth dependent set.
- **Weekly trigger.** No new schedule — the `go`/`nuget` variants are already child workflows of
`bootstrapOsspckgs`, which the existing `osspckgs-bootstrap-weekly` schedule runs incrementally.

Related: ADR-0003 (deps BQ table selection — same root cause: GO/NUGET absent from the resolved-graph tables).
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Use the `/adr` skill in Claude Code to record new ADRs or query past decisions.
| --------------------------------------------------- | ---------------------------------------- | ------ | ---------- |
| [ADR-0001](./0001-oss-packages-design-decisions.md) | OSS packages — design decisions (living) | living | 2026-05-27 |
| [ADR-0003](./0003-deps-bq-table-selection.md) | Use DependencyGraphEdgesLatest for deps ingestion; defer DependenciesLatest until NUGET or GO needed | accepted | 2026-05-29 |
| [ADR-0004](./0004-go-nuget-transitive-dependent-counts.md) | Compute GO/NUGET transitive dependent counts via exact reverse closure (over HLL approximation) | accepted | 2026-06-23 |

## Why ADRs?

Expand Down
20 changes: 0 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion services/apps/packages_worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
"devDependencies": {
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^20.8.2",
"@types/pg-copy-streams": "^1.2.5",
"@types/semver": "^7.5.8",
"@types/unzipper": "^0.10.10",
Comment thread
themarolt marked this conversation as resolved.
"nodemon": "^3.0.1",
Expand Down
2 changes: 0 additions & 2 deletions services/apps/packages_worker/src/bin/bq-dataset-ingest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { scheduleOsspckgsBootstrap } from '../deps-dev/schedules/bootstrap'
import { scheduleOsspckgsCleanup } from '../schedules/cleanup'
Comment thread
themarolt marked this conversation as resolved.
import { svc } from '../service'

setImmediate(async () => {
await svc.init()
await scheduleOsspckgsBootstrap()
await scheduleOsspckgsCleanup()
await svc.start()
})
4 changes: 3 additions & 1 deletion services/apps/packages_worker/src/cargo/loadDump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ async function copyCsv(
): Promise<number> {
const con = await db.connect()
try {
// @types/pg-copy-streams Submittable doesn't match @types/pg's query overload.
// pg-promise's bundled IClient.query types only return Promise<IResult> — they lack node-pg's
// `query<T extends Submittable>(qs: T): T` stream overload. At runtime con.client IS a node-pg
// Client and returns the CopyStreamQuery, so cast through to recover it.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stream: CopyStreamQuery = (con.client as any).query(
copyFrom(`COPY ${STAGING_SCHEMA}.${table} FROM STDIN CSV HEADER`),
Expand Down
33 changes: 19 additions & 14 deletions services/apps/packages_worker/src/deps-dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ See `src/scripts/triggerBootstrap.ts --help` for full options.

Each job kind has a hardcoded `maxBytesGb` ceiling checked via a BQ dry-run
before the real export fires. If the dry-run exceeds the ceiling the activity
fails immediately (no BQ cost incurred).
fails immediately (no BQ cost incurred). Script-mode kinds (the GO/NUGET reverse
transitive closure — a multi-statement `WHILE` loop a dry-run cannot price) skip
the dry-run and instead enforce the ceiling server-side via `maximumBytesBilled`;
there the ceiling is a runaway cap set above expected spend, not a tight gate.

Every ceiling can be overridden at runtime via an env variable — useful when a
table grows past the default ceiling without requiring a code deploy:
Expand All @@ -45,19 +48,21 @@ The mode-specific key takes precedence over the generic key. Value must be a pos

**When adding a new job kind, add a row to this table.**

| Env var override | Default (GB) | Job kind | Notes |
| -------------------------------------------------- | -----------: | ---------------------- | --------------------------------------------------------- |
| `BQ_DATASET_INGEST_PACKAGES_FULL_MAX_BQ_GB` | 6000 | `packages` | Full only (set in `ingestPackages.ts`) |
| `BQ_DATASET_INGEST_PACKAGES_INCREMENTAL_MAX_BQ_GB` | 400 | `packages` | Incremental only (set in `ingestPackages.ts`) |
| `BQ_DATASET_INGEST_VERSIONS_MAX_BQ_GB` | 400 | `versions` | |
| `BQ_DATASET_INGEST_PACKAGE_DEPENDENCIES_MAX_BQ_GB` | 10000 | `package_dependencies` | Incremental always scans ~2 full-day partitions (~3.85TB) |
| `BQ_DATASET_INGEST_REPOS_MAX_BQ_GB` | 2000 | `repos` | |
| `BQ_DATASET_INGEST_PACKAGE_REPOS_MAX_BQ_GB` | 2000 | `package_repos` | |
| `BQ_DATASET_INGEST_ADVISORIES_MAX_BQ_GB` | 10 | `advisories` | |
| `BQ_DATASET_INGEST_ADVISORY_PACKAGES_MAX_BQ_GB` | 1500 | `advisory_packages` | |
| `BQ_DATASET_INGEST_DEPENDENT_COUNTS_MAX_BQ_GB` | 2000 | `dependent_counts` | |
| `BQ_DATASET_INGEST_SCORECARD_REPOS_MAX_BQ_GB` | 50 | `scorecard_repos` | |
| `BQ_DATASET_INGEST_SCORECARD_CHECKS_MAX_BQ_GB` | 500 | `scorecard_checks` | |
| Env var override | Default (GB) | Job kind | Notes |
| ---------------------------------------------------- | ----------------------: | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BQ_DATASET_INGEST_PACKAGES_FULL_MAX_BQ_GB` | 6000 | `packages` | Full only (set in `ingestPackages.ts`) |
| `BQ_DATASET_INGEST_PACKAGES_INCREMENTAL_MAX_BQ_GB` | 400 | `packages` | Incremental only (set in `ingestPackages.ts`) |
| `BQ_DATASET_INGEST_VERSIONS_MAX_BQ_GB` | 400 | `versions` | |
| `BQ_DATASET_INGEST_PACKAGE_DEPENDENCIES_MAX_BQ_GB` | 25000 full / 10000 incr | `package_dependencies` | Full scans `*Latest`. Incremental is a snapshot edge-diff (today vs watermark partitions of `DependencyGraphEdges` + `GoRequirements` + `NuGetRequirements`), matched on `(root, to_name)` excluding the resolved `to_version` to drop re-resolution churn (~4.1TB, Option A). Mode-specific `…_FULL_…` / `…_INCREMENTAL_…` keys take precedence. |
| `BQ_DATASET_INGEST_REPOS_MAX_BQ_GB` | 2000 | `repos` | |
| `BQ_DATASET_INGEST_PACKAGE_REPOS_MAX_BQ_GB` | 2000 | `package_repos` | |
| `BQ_DATASET_INGEST_ADVISORIES_MAX_BQ_GB` | 10 | `advisories` | |
| `BQ_DATASET_INGEST_ADVISORY_PACKAGES_MAX_BQ_GB` | 1500 | `advisory_packages` | |
| `BQ_DATASET_INGEST_DEPENDENT_COUNTS_MAX_BQ_GB` | 2000 | `dependent_counts` | Edges only (NPM/MAVEN/PYPI/CARGO) from the `Dependents` reverse index. GO/NUGET are absent from `Dependents` and run as separate kinds below. |
| `BQ_DATASET_INGEST_DEPENDENT_COUNTS_GO_MAX_BQ_GB` | 5000 | `dependent_counts_go` | GO exact reverse transitive closure over `GoRequirementsLatest` (script mode). All 3 count columns. Ceiling is a `maximumBytesBilled` runaway cap above the validated full-pipeline spend (2.31 TB incl. repos aggregation), not a dry-run gate. |
| `BQ_DATASET_INGEST_DEPENDENT_COUNTS_NUGET_MAX_BQ_GB` | 200 | `dependent_counts_nuget` | NUGET exact reverse transitive closure over `NuGetRequirementsLatest` (script mode). All 3 count columns. `maximumBytesBilled` runaway cap above the measured ~32 GB. |
| `BQ_DATASET_INGEST_SCORECARD_REPOS_MAX_BQ_GB` | 50 | `scorecard_repos` | |
| `BQ_DATASET_INGEST_SCORECARD_CHECKS_MAX_BQ_GB` | 500 | `scorecard_checks` | |

The override logic lives in `src/deps-dev/activities/bqExportToGcs.ts`.

Expand Down
Loading
Loading