Repository navigation
Emit dimension-batch digests in a deterministic order (#1801) - #1803
Merged
Merged
Conversation
A production field instance logged nine self-healing 40P01s in its first night on the v38 dedup write path and zero before it; its pg.log lock graphs show two sessions on the same statement shape waiting on tuples the other holds in collect.query_text_dim. The batch upsert binds arrays drained from a Dictionary in enumeration order -- per-session ARRIVAL order, not a total order across sessions. Two collection cycles whose batches share two digests in opposite relative order each take the row lock the other needs next. The within-batch dedup is orthogonal and was never meant to help: it makes a batch conflict-free with ITSELF (the 21000 guard), not ordered against its siblings. ToArrays now emits ascending digest order, which fixes query_plan_dim by the same edit rather than waiting for its name to appear in a log. Sorted client-side rather than by an ORDER BY in the statement so the lock order is a property of the values bound, owing nothing to how the planner chooses to execute the insert -- and so it stays checkable without a database. Ordinal sort on the hex key IS byte-wise digest order (equal-length digests; hex digits ascend in ASCII as the nibbles they encode), though the test asserts raw bytes so the claim does not rest on the implementation's key representation. module_map is checked and untouched: the ORDER BY its DISTINCT ON requires is already a deterministic ascending order on its conflict key. That reasoning is now a comment there, because an ORDER BY that looks purely about picking the latest row is exactly the kind a later edit removes. Pinned twice, because the ordering is invisible to every behavioural test -- an unordered batch upserts byte-identical data and only the lock order differs: a unit test feeding deliberately reversed digests and requiring ascending bytes out, and a source pin that the sort exists. Both watched red on a COMPILING build by restoring the dictionary enumeration. No live deadlock repro is claimed. Reproducing a two-session lock cycle on demand is timing-dependent, and a test that passes because the race did not occur is worse than no test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 28, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1801.
A production field instance logged nine self-healing
40P01s in its first night on the v38 dedup write path, and zero before it. Its ownpg.logcaptured two full lock graphs: two sessions on the same statement shape, each waiting on a tuple the other held incollect.query_text_dim.Mechanism
The dim upsert binds a batch through
unnest, and the arrays came out of aDictionaryin enumeration order — per-session arrival order, which is not a total order across sessions. Two collection cycles whose batches share two digests in opposite relative order each take the row lock the other needs next, and the cycle closes.The within-batch digest dedup does not help here, and was never meant to: it makes a batch conflict-free with itself (the SQLSTATE 21000 guard), not ordered against its siblings. Two different properties that happen to concern the same key — which is why the dedup being correct and the batch still deadlocking are not in tension.
Mechanism chosen: client-side sort in
ToArraysOne deterministic total order, imposed at the one point every caller shares — so
query_plan_dimis fixed by the same edit rather than waiting for its name to appear in a log.Client-side rather than
ORDER BY u.digestin the statement, for two reasons:ORDER BYfeedingINSERT … ON CONFLICTis a strong hint about row processing order, not a guarantee about lock acquisition — and this is a correctness property, not a performance one.The statement's doc now records the requirement and points at where it is imposed, so the SQL is still self-documenting about what it depends on.
Ordinal sort on the hex key is byte-wise digest order: every digest is the same length, and hex digits ascend in ASCII in the same sequence as the nibbles they encode. The test asserts the raw bytes anyway, so the claim does not rest on the representation the implementation happens to key by.
The hour-throttled conditional
DO UPDATEis untouched.collect.module_map— checked, deliberately unchangedDarlingModuleMap.RefreshSqlcarriesORDER BY server_name, sql_handle, collection_time DESCbecause itsDISTINCT ONrequires it — and that is already a deterministic ascending order on its conflict key, so concurrent refreshes take the locks in the same relative order and this cycle cannot form there. Added as a comment cross-reference only, because anORDER BYthat reads as purely about picking the latest row is exactly the kind a later edit removes as redundant.Pins — the ordering is invisible to behavioural tests
An unordered batch upserts byte-identical data; only the lock-acquisition order differs. Every round-trip, dedup and live test passes either way. So it is pinned twice:
ToArraysat all, andentries.Valuesmust be gone.entries.Valuesenumeration (build verified clean first)Assert.Contains() Failure: Not found: "OrderBy"; behavioural:Assert.Equal() Failure: Collections differ at index 0 / Expected: "04A9E731…" / Actual: "D2F45524…"The build was confirmed to succeed before reading the mutation's verdict — a mutation that fails to compile runs the previous binary and is indistinguishable from a working guard.
No live deadlock repro is claimed. Reproducing a two-session lock cycle on demand is timing-dependent, and a test that passes because the race did not happen is worse than no test. The property under guard is the total order, which is exactly what these two pin.
Verification
PerformanceMonitor.sln-t:Rebuild -c Debug: 0 Warning(s) 0 Error(s).SearchPath=collect,config), so the gated legs ran rather than skipped: 3,742 passed / 0 failed / 8 skipped.Darling/and no shared collector code.Generated with Claude Code