Skip to content

Commit d89f9fd

Browse files
committed
fix: serialize OSV/deps.dev advisory range writes with row locks (CM-1258)
Signed-off-by: Uroš Marolt <uros@marolt.me>
1 parent 1978360 commit d89f9fd

3 files changed

Lines changed: 31 additions & 2 deletions

File tree

services/apps/packages_worker/src/deps-dev/workflows/ingestAdvisories.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,24 @@ WHERE NOT EXISTS (
109109
ON CONFLICT (advisory_package_id, COALESCE(introduced_version, ''), COALESCE(fixed_version, ''), COALESCE(last_affected, '')) DO NOTHING
110110
`
111111

112+
// Runs as prepareSql (uncounted) ahead of ADVISORY_AFFECTED_RANGES_MERGE_SQL, in the
113+
// same transaction. Row-locks every advisory_packages this chunk will touch, in id
114+
// order, before the NOT EXISTS ownership check runs. OSV's upsertOne (services/apps/
115+
// packages_worker/src/osv/upsertAdvisory.ts) takes the matching per-row lock before it
116+
// writes advisory_affected_ranges for that advisory_package, so whichever transaction
117+
// (deps.dev chunk or OSV record) locks the row first now forces the other to wait and
118+
// see its committed writes — closing the race the two independently-scheduled write
119+
// paths would otherwise have on the ownership check (ADR-0001 §Write semantics).
120+
const ADVISORY_PACKAGES_LOCK_SQL = `
121+
SELECT ap.id
122+
FROM advisory_packages ap
123+
JOIN staging.osspckgs_advisory_packages_raw s
124+
ON s.ecosystem = ap.ecosystem AND s.package_name = ap.package_name
125+
JOIN advisories adv ON adv.osv_id = s.osv_id AND adv.id = ap.advisory_id
126+
ORDER BY ap.id
127+
FOR UPDATE OF ap
128+
`
129+
112130
const ADVISORIES_PG_COLUMNS = [
113131
'osv_id',
114132
'source',
@@ -274,6 +292,7 @@ export async function ingestAdvisories(opts: {
274292

275293
const { rowsAffected, tableRowCounts } = await mergeStagingToTable({
276294
jobId: pkgsExport.jobId,
295+
prepareSql: ADVISORY_PACKAGES_LOCK_SQL,
277296
mergeSql: [ADVISORY_PACKAGES_MERGE_SQL, ADVISORY_AFFECTED_RANGES_MERGE_SQL],
278297
tableNames: ['advisory_packages', 'advisory_affected_ranges'],
279298
isFinal,

services/apps/packages_worker/src/osv/upsertAdvisory.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ async function upsertOne(qx: QueryExecutor, record: NormalizedRecord): Promise<v
4646
packageName: entry.pkg.packageName,
4747
})
4848

49+
// Row-locks this advisory_packages row (held until the enclosing transaction
50+
// commits) before touching advisory_affected_ranges, matching the lock
51+
// deps.dev's bulk merge takes on the same row (ADVISORY_PACKAGES_LOCK_SQL in
52+
// ingestAdvisories.ts) — whichever writer locks first forces the other to
53+
// wait and see its committed writes, closing the ownership race between the
54+
// two independently-scheduled write paths (ADR-0001 §Write semantics).
55+
await qx.result(`SELECT id FROM advisory_packages WHERE id = $(advisoryPackageId) FOR UPDATE`, {
56+
advisoryPackageId,
57+
})
58+
4959
await reconcileOsvRanges(
5060
qx,
5161
advisoryPackageId,

services/libs/tinybird/pipes/ossPackages_enriched.pipe

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ SQL >
9292
FROM ossPackages_enriched_packages AS p
9393
INNER JOIN advisoryPackages AS ap FINAL ON ap.packageId = p.id
9494
INNER JOIN advisories AS a FINAL ON a.id = ap.advisoryId
95-
INNER JOIN advisoryAffectedRanges AS aar FINAL ON aar.advisoryPackageId = ap.id
96-
AND aar.deletedAt IS NULL
95+
INNER JOIN
96+
advisoryAffectedRanges AS aar FINAL ON aar.advisoryPackageId = ap.id AND aar.deletedAt IS NULL
9797
LEFT JOIN
9898
(SELECT packageId, number, publishedAt AS introducedAt FROM versions FINAL) AS vi
9999
ON vi.packageId = p.id

0 commit comments

Comments
 (0)