Gap
cancelBlobReclamation (added in #2145) drops a queued blob reclamation when a record version being written references the file again. It runs during msgpackr encoding — before the write commits — so an aborted transaction leaves the file both unreferenced and unqueued.
Sequence:
- Write A supersedes blob
F. On commit, F is queued for reclamation.
- Write B encodes a record that references
F again. cancelBlobReclamation removes the queued entry at encode time.
- B aborts.
F is referenced by no live record (A's supersession stands) and is no longer queued for reclamation.
F then survives until someone runs cleanup_orphan_blobs. No data is lost — this is a reclaimable leak, not a dangling reference — but the file is retained indefinitely without operator action.
Why it was left this way
Cancelling at commit instead of at encode would close the leak but open a worse window: between encode and commit, the reclaimer could unlink F while B is committing a record that points at it, producing exactly the dangling reference #2145 exists to prevent. Given the choice between a reclaimable leak on abort and a dangling reference on commit, #2145 took the leak.
Suggested fix
Make the cancellation transactional rather than speculative: have the encoder record which reclamations it cancelled for a given write, and re-queue them from the transaction's abort path. That mirrors how newly-written blobs are already handled on abort (cleanupUnusedBlobs(write.savedBlobs, …) in LMDBTransaction.ts:277/:323), so the lifecycle hook already exists — this is plumbing from the encoder into it, not a new mechanism.
Re-queueing (rather than unlinking directly) is the safe direction: the file returns to the normal reclamation path, where the snapshot watermark, holds, and retention window all still apply before anything is unlinked.
Scope
- Narrow: requires a write that re-references a blob already superseded by an earlier committed write, and that write then aborting.
- No data-loss risk in either the current behavior or the fix.
- Test shape: supersede a blob, begin a write that re-references it, abort, then assert the file is reclaimed without an orphan sweep.
Related
🤖 Generated by Claude (claude-opus-5)
Gap
cancelBlobReclamation(added in #2145) drops a queued blob reclamation when a record version being written references the file again. It runs during msgpackr encoding — before the write commits — so an aborted transaction leaves the file both unreferenced and unqueued.Sequence:
F. On commit,Fis queued for reclamation.Fagain.cancelBlobReclamationremoves the queued entry at encode time.Fis referenced by no live record (A's supersession stands) and is no longer queued for reclamation.Fthen survives until someone runscleanup_orphan_blobs. No data is lost — this is a reclaimable leak, not a dangling reference — but the file is retained indefinitely without operator action.Why it was left this way
Cancelling at commit instead of at encode would close the leak but open a worse window: between encode and commit, the reclaimer could unlink
Fwhile B is committing a record that points at it, producing exactly the dangling reference #2145 exists to prevent. Given the choice between a reclaimable leak on abort and a dangling reference on commit, #2145 took the leak.Suggested fix
Make the cancellation transactional rather than speculative: have the encoder record which reclamations it cancelled for a given write, and re-queue them from the transaction's abort path. That mirrors how newly-written blobs are already handled on abort (
cleanupUnusedBlobs(write.savedBlobs, …)inLMDBTransaction.ts:277/:323), so the lifecycle hook already exists — this is plumbing from the encoder into it, not a new mechanism.Re-queueing (rather than unlinking directly) is the safe direction: the file returns to the normal reclamation path, where the snapshot watermark, holds, and retention window all still apply before anything is unlinked.
Scope
Related
cancelBlobReclamationinresources/blob.tsand its docstring, which records this trade-off).removeEntry.🤖 Generated by Claude (claude-opus-5)