|
3 | 3 | The Packagist worker keeps the PHP/Composer slice of the packages database fresh |
4 | 4 | by crawling **packagist.org directly** — deps.dev has no Packagist coverage, so |
5 | 5 | unlike npm/maven/pypi there is no BigQuery universe to import from; the registry |
6 | | -crawl _is_ the universe source. It runs **four Temporal workflows** on the |
7 | | -`packagist-worker` task queue: three cron schedules registered at worker boot |
8 | | -(`src/bin/packagist-worker.ts`), plus the metadata drain, which the seed chains |
9 | | -as a child workflow on completion. |
| 6 | +crawl _is_ the universe source. It runs **six Temporal workflows** on the |
| 7 | +`packagist-worker` task queue: four cron schedules registered at worker boot |
| 8 | +(`src/bin/packagist-worker.ts` — including the transitive backstop), plus two |
| 9 | +event-chained drains — the metadata drain (chained off the seed) and the |
| 10 | +transitive-dependents closure (chained off the metadata drain). |
10 | 11 |
|
11 | 12 | Identity: ecosystem `packagist`, purls `pkg:composer/{vendor}/{name}` |
12 | 13 | (namespace = vendor). Audit tag: `packagist` in `audit_field_changes`. |
@@ -115,6 +116,11 @@ touching p2. |
115 | 116 | `If-Modified-Since` next run). |
116 | 117 | - `audit_field_changes` — every changed field above, worker tag `packagist`. |
117 | 118 |
|
| 119 | +On natural completion (not in `STOP_AFTER_FIRST_PAGE` debug runs) the drain |
| 120 | +**chain-starts the transitive-dependents closure** (§5) — freshly refreshed |
| 121 | +edges are what the closure consumes, so it follows the drain as an event, not |
| 122 | +a clock offset. |
| 123 | + |
118 | 124 | --- |
119 | 125 |
|
120 | 126 | ## 3. `ingestPackagistDownloads30d` — monthly rolling-window capture |
@@ -166,11 +172,70 @@ last. State: `daily_downloads_last_run_at` + `daily_downloads_run_result`. |
166 | 172 |
|
167 | 173 | --- |
168 | 174 |
|
| 175 | +## 5. `computePackagistTransitiveDependents` — weekly reverse-closure counts |
| 176 | + |
| 177 | +**Schedule:** primary trigger is the **chain off the metadata drain's natural |
| 178 | +completion** (effectively weekly, after the Sunday drain finishes), the same |
| 179 | +event-not-clock idiom as seed → metadata. A **ledger-gated backstop cron** |
| 180 | +(`packagist-transitive-backstop`, Monday 04:41 UTC) covers broken weeks: it |
| 181 | +no-ops when a run completed within 6 days or while the metadata drain is still |
| 182 | +crawling (whose completion chains the closure itself), otherwise chain-starts |
| 183 | +the same fixed workflow id — so it can never race a live drain and a healthy |
| 184 | +week never pays a second scan. Recover manually with |
| 185 | +`pnpm trigger-packagist:local transitive`. |
| 186 | +**Targets:** every packagist package (the merge zero-fills leaves). |
| 187 | + |
| 188 | +**What it does:** three steps — |
| 189 | + |
| 190 | +1. **Snapshot** — collapses the packagist slice of `package_dependencies` into |
| 191 | + `staging.packagist_transitive_edges`: distinct package-level pairs, `require` |
| 192 | + edges only (`require-dev` excluded — Composer does not install dev deps |
| 193 | + transitively), self-edges dropped. This is the only step touching the full |
| 194 | + ~1.5B-row table; `package_id` has no index there, so it is a deliberate |
| 195 | + weekly parallel seq scan (same access pattern the criticality PageRank loader |
| 196 | + already uses). |
| 197 | +2. **Closure** — the exact reverse transitive closure in one recursive |
| 198 | + statement into `staging.packagist_transitive_counts` (~29M reachable pairs, |
| 199 | + ~51 s measured on the full dataset). Cycles terminate; a package is never |
| 200 | + counted as its own dependent. |
| 201 | +3. **Merge** — keyset batches of 10K into |
| 202 | + `packages.transitive_dependent_count`, zero-filling packages with no |
| 203 | + dependents (`0` = "computed, none" vs NULL = "never computed"). |
| 204 | + `IS DISTINCT FROM` keeps re-runs churn-free — `last_synced_at` (the |
| 205 | + Sequin/Tinybird signal) moves only on real changes. |
| 206 | + |
| 207 | +**Populates:** `packages.transitive_dependent_count` for packagist rows only — |
| 208 | +dependents reachable **only at depth ≥ 2**; direct dependents are excluded, |
| 209 | +matching the deps.dev `MinimumDepth > 1` convention every other ecosystem uses. |
| 210 | +The registry-reported `dependent_count` is untouched. **No audit rows** — bulk |
| 211 | +derived analytics, same policy as the deps.dev dependent-counts merges. Run |
| 212 | +state: one `packagist_transitive_runs` row per run |
| 213 | +(`pending → merging → done | failed`, with graph sizes and merge totals) — |
| 214 | +per-purl `packagist_package_state` doesn't fit a whole-ecosystem batch, and |
| 215 | +`osspckgs_ingest_jobs` is the BQ-ingest ledger. An empty edge snapshot |
| 216 | +hard-aborts the run (`failed`) instead of writing zeros over good data; the |
| 217 | +merge itself refuses an empty counts table — the staging tables are UNLOGGED |
| 218 | +(truncated by crash recovery), and a mid-drain truncation must never be |
| 219 | +zero-filled over real counts — and a merge phase that fails permanently marks |
| 220 | +the run `failed` rather than leaving it in `merging`. |
| 221 | + |
| 222 | +**Why:** deps.dev has zero Packagist coverage, so no external source can supply |
| 223 | +this signal. `rank_packages()` already has `transitive_dependents` wired in but |
| 224 | +drops it while the ecosystem total is zero — the first pass after this lane |
| 225 | +runs picks it up automatically, and since `is_critical` is a BOOL_OR across |
| 226 | +signals it can only add critical packages, never remove any. |
| 227 | + |
| 228 | +**Known undercounts (conservative by design):** edges exist only where the |
| 229 | +dependency target resolved to a known packages row (unresolved targets are |
| 230 | +skipped at ingest), and packages with no stored edges count as leaves. |
| 231 | + |
| 232 | +--- |
| 233 | + |
169 | 234 | ## What this worker deliberately does NOT write |
170 | 235 |
|
171 | | -- `transitive_dependent_count`, `dependent_repos_count` — not computable from |
172 | | - the registry; needs a reverse-closure over our stored direct edges |
173 | | - (future work, see ADR-0009 risks). |
| 236 | +- `dependent_repos_count` — not computable from the registry; needs a |
| 237 | + package→repo mapping across the dependent set (deps.dev provides this for |
| 238 | + its ecosystems; Packagist has no equivalent). |
174 | 239 | - Advisories — the OSV worker owns security data platform-wide. |
175 | 240 | - `is_critical` / `criticality_score` / ranking columns — the shared |
176 | 241 | criticality worker; this worker only _reads_ `is_critical` for scoping. |
@@ -199,14 +264,17 @@ DEV=1 ./scripts/cli service packagist-worker up |
199 | 264 | # trigger on demand instead of waiting for the crons |
200 | 265 | cd services/apps/packages_worker |
201 | 266 | pnpm trigger-packagist:local seed # discovery (chain-starts metadata!) |
202 | | -pnpm trigger-packagist:local metadata # enrichment: info + versions + deps |
| 267 | +pnpm trigger-packagist:local metadata # enrichment: info + versions + deps (chain-starts transitive!) |
203 | 268 | pnpm trigger-packagist:local downloads-30d # monthly rolling-window capture |
204 | 269 | pnpm trigger-packagist:local downloads-daily # daily capture, critical slice |
| 270 | +pnpm trigger-packagist:local transitive # reverse-closure transitive dependent counts |
205 | 271 | ``` |
206 | 272 |
|
207 | 273 | Note: triggering `seed` also chain-starts the full `metadata` drain (set |
208 | 274 | `CROWD_PACKAGES_PACKAGIST_STOP_AFTER_FIRST_PAGE=true` locally to bound it). |
209 | | -Local smoke order: seed → metadata → (rank) → downloads lanes for |
210 | | -critical-scoped writes. State lives in `packagist_package_state` |
211 | | -(migration `V1784314023__packagist_worker.sql`); design decisions in |
| 275 | +Local smoke order: seed → metadata → transitive → (rank) → downloads lanes for |
| 276 | +critical-scoped writes. Per-purl state lives in `packagist_package_state` |
| 277 | +(migration `V1784314023__packagist_worker.sql`); the transitive lane tracks its |
| 278 | +runs in `packagist_transitive_runs` (migration |
| 279 | +`V1785740540__packagist_transitive_runs.sql`). Design decisions in |
212 | 280 | `docs/adr/0009-packagist-worker-design-decisions.md`. |
0 commit comments