11import {
2+ AdvisoryRangeInsertInput ,
23 findPackageId ,
34 reconcileOsvRanges ,
45 supersedeDepsDevRanges ,
@@ -36,6 +37,7 @@ async function upsertOne(qx: QueryExecutor, record: NormalizedRecord): Promise<v
3637
3738 const advisoryId = await upsertAdvisory ( qx , advisory )
3839
40+ const entries : { advisoryPackageId : number ; ranges : AdvisoryRangeInsertInput [ ] } [ ] = [ ]
3941 for ( const entry of packages ) {
4042 const packageId = await findPackageId ( qx , entry . pkg )
4143
@@ -46,26 +48,37 @@ async function upsertOne(qx: QueryExecutor, record: NormalizedRecord): Promise<v
4648 packageName : entry . pkg . packageName ,
4749 } )
4850
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-
59- await reconcileOsvRanges (
60- qx ,
51+ entries . push ( {
6152 advisoryPackageId,
62- dedupeRanges ( entry . ranges ) . map ( ( range ) => ( {
53+ ranges : dedupeRanges ( entry . ranges ) . map ( ( range ) => ( {
6354 advisoryPackageId,
6455 introducedVersion : range . introducedVersion ,
6556 fixedVersion : range . fixedVersion ,
6657 lastAffected : range . lastAffected ,
6758 } ) ) ,
68- )
59+ } )
60+ }
61+
62+ // Lock advisory_packages rows in ascending id order — the same order deps.dev's
63+ // bulk merge locks them in (ADVISORY_PACKAGES_LOCK_SQL's ORDER BY ap.id in
64+ // ingestAdvisories.ts). OSV's payload order is otherwise arbitrary; locking out
65+ // of order here would let this per-record transaction and a concurrent deps.dev
66+ // chunk each hold one row and wait on the other's, deadlocking — and deps.dev's
67+ // maximumAttempts: 1 turns a deadlock abort into a hard merge failure, not a retry.
68+ entries . sort ( ( a , b ) => a . advisoryPackageId - b . advisoryPackageId )
69+
70+ for ( const { advisoryPackageId, ranges } of entries ) {
71+ // Row-locks this advisory_packages row (held until the enclosing transaction
72+ // commits) before touching advisory_affected_ranges, matching the lock
73+ // deps.dev's bulk merge takes on the same row — whichever writer locks first
74+ // forces the other to wait and see its committed writes, closing the
75+ // ownership race between the two independently-scheduled write paths
76+ // (ADR-0001 §Write semantics).
77+ await qx . result ( `SELECT id FROM advisory_packages WHERE id = $(advisoryPackageId) FOR UPDATE` , {
78+ advisoryPackageId,
79+ } )
80+
81+ await reconcileOsvRanges ( qx , advisoryPackageId , ranges )
6982 await supersedeDepsDevRanges ( qx , advisoryPackageId )
7083 }
7184}
0 commit comments