🐛 fix(availability): prove references before reclamation readiness - #2012
Merged
Conversation
A reclamation pass scanned the reference inventory once and reused that set for both phases. A publish landing between the phases never appeared in it, so finalization could mark a tombstone ready while current metadata still named its digest. The inventory spans several owner reads that cannot share one redb transaction, so a revision now brackets it: every driver-row write advances the counter, the pass reads it on both sides of the scan, and a scan the counter moved under is discarded rather than trusted. Each reclamation write carries the revision its verdict came from, and the compare-and-put rejects a write whose proof the store has already outrun. Selection and readiness each prove their own inventory, so the readiness verdict no longer inherits the opening scan. The counter lives in its own table rather than the journal serial because a driver transaction can add or remove a reference without appending a journal entry, which leaves that serial standing still across exactly the mutation the proof has to catch. It advances on any driver-row write, since only the owners know which keys hold references and shared storage cannot ask them; a pass that loses the race keeps its cursors and re-proves on the next one. Closes #1382
gaborbernat
force-pushed
the
fix/refresh-reference-proof-1382
branch
from
August 31, 2026 18:15
a7b552c to
9b557a6
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
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.
A reclamation pass built its reference inventory once at the top and reused that same set when it finalized tombstones, so the verdict the finalizer wrote omitted any publish that landed between the two phases. The digest could reach
Readywhile current metadata still named it, which is the concurrent-reference case #571 rules out. The paging work in #1997 made both phases read one bounded page, so re-proving the inventory once per phase costs a scan rather than a scan per tombstone.An inventory spans several owner reads that cannot share one redb transaction, so the pass brackets it with a revision instead.
MetaStore::reference_revisionreturns a counter that every driver-row write advances.reclaim_passreads that counter on both sides of the scan and discards a scan it moved under. Each reclamation write then carries the revision its verdict came from.compare_and_put_reclamation_tombstonecompares that revision inside its write transaction and answersTombstoneWrite::ReferencesMovedonce the counter has moved, which retires the verdict without writing it. Selection and readiness each prove their own inventory, so the readiness verdict no longer inherits the opening scan.The counter lives in its own table rather than reusing the journal serial, because a driver transaction can add or remove a reference without appending a journal entry, and that serial does not move across the one mutation the proof has to catch. It advances on any driver-row write, since owners choose their own key layouts and shared storage cannot ask them which prefixes hold references. Counting too many writes costs a retired scan; counting too few costs bytes. A pass that loses the race returns early with its cursors untouched, so the next pass re-proves and repeats the page.
flowchart LR R0[Read revision] --> Scan[Scan owner references] Scan --> R1{Revision moved?} R1 -- yes --> Drop[Discard the scan] R1 -- no --> Verdict[Decide referenced] Verdict --> CAP{Revision still held?} CAP -- no --> Drop CAP -- yes --> Write[Write the tombstone] classDef accent fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a; classDef warn fill:#ffe3a3,stroke:#d29200,color:#3a2c00; class R0,Scan,Verdict,Write accent; class R1,CAP,Drop warn;ReclamationStore::compare_and_put_reclamation_tombstonenow takes the proved revision and returnsTombstoneWritein place of a barebool, andselect_reclamation_candidateandmark_reclamation_readyreturnOptionso a caller can tell a retired proof from a decided digest. Reclaim guards and the destructive interval after readiness stay as they are for #1383, whosereclaim_guard_serialstill reads the journal serial this change shows is not a reference proof.Closes #1382