Repository navigation
Stop RocksDB eviction from deleting renewed records or the blobs of records that survive - #3413
Merged
Merged
Conversation
…commits Refs #2861 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01227hE2ocnUgq4RfTawEXQ8 Dispatch-Task: harper-evict-rocksdb-version-gate-blob-commit
…y after commit evict() read the entry in its transaction and compared the caller's version only on audited or sourced tables; elsewhere it removed whatever version was live, deleting a renewed record, orphaning its new index keys and unlinking its blobs. Its RocksDB removal also went through removeEntry, which queues blob reclamation at staging, so an eviction abandoned on ERR_BUSY still reclaimed a blob the surviving record references. Refs #2861 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01227hE2ocnUgq4RfTawEXQ8 Dispatch-Task: harper-evict-rocksdb-version-gate-blob-commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01227hE2ocnUgq4RfTawEXQ8 Dispatch-Task: harper-evict-rocksdb-version-gate-blob-commit
A resequenced write can reuse the version the caller read, so the caller's copy may not be the record being removed; the eviction batcher already indexes from the fresh entry. Refs #3384 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01227hE2ocnUgq4RfTawEXQ8 Dispatch-Task: harper-evict-rocksdb-version-gate-blob-commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01227hE2ocnUgq4RfTawEXQ8 Dispatch-Task: harper-evict-rocksdb-version-gate-blob-commit
…matches the stored record Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01227hE2ocnUgq4RfTawEXQ8 Dispatch-Task: harper-evict-rocksdb-version-gate-blob-commit
Contributor
There was a problem hiding this comment.
Code Review
This pull request updates the record eviction logic (evict()) in resources/Table.ts to ensure that blob files are only reclaimed after the eviction transaction successfully commits, preventing premature deletion in case of conflicts or abandoned evictions. It also updates resources/DESIGN.md to document these changes and adds a comprehensive test suite in unitTests/resources/evictionBlobs.test.js. The review feedback points out a style guide violation in the new test file, where Node.js built-in modules (fs, crypto, and timers/promises) should be imported using the node: prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01227hE2ocnUgq4RfTawEXQ8 Dispatch-Task: harper-evict-rocksdb-version-gate-blob-commit
kriszyp
marked this pull request as ready for review
October 23, 2026 00:02
This was referenced Oct 23, 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.
⊙ Problem
On RocksDB,
Table.evict()could delete data it should have kept:🤔 Is the evict-only fix the right scope?
Yes, as the task specified.
_writeDeleteandupdateRecordstill delete blobs before commit (#2861, #3367). The shared fix belongs inremoveEntry/DatabaseTransaction, not in each caller.💡 Solution
evict()reads the record in its own transaction and skips any version except the one its caller read, on every table.⚖️ Alternatives
removeEntrywait for the commit on every RocksDB transactional delete. The root-cause fix for Aborted delete leaks blob bytes: removeEntry's unlink gate keys off the removal's own commit, not the outer transaction #2861, also covering_writeDeleteand the batcher; it changes shared delete behavior, so it is a separate PR.remove()resolve at commit. Out of scope, and Harper callsremoveSyncfor these deletes anyway.🤔 Accept the narrower race that post-commit deletion keeps?
Deleting after commit prevents blob loss on commit conflicts. A narrower race remains, shared by LMDB eviction and source-miss removal: a write that re-encodes the same blob between commit and cleanup can lose its re-reference flag. The fix belongs in
blob.ts(#3382).🤔 Leave reused versions to #3384?
A resequenced write can reuse the version the caller read, and the gate still evicts that record, as on main and in the batcher. Refusing reused versions changes when expired records leave, so it is separate.
🔧 Changes
resources/Table.tsevict(): the version gate now runs on every table, and theentry ?? getEntry(id)fallbacks that read the live record are gone. Both engines index from the entry it read.removeSyncand deletes blobs only from the commit's success handler; evictions without blobs keep the rejection-only path. The cleanup-scan comment now says why blob records stay on the per-record path.resources/DESIGN.md: the blob-deletion note now lists RocksDBevict()with the paths that delete after commit, and records the unconditional version gate.✅ Verification
unitTests/resources/evictionBlobs.test.jsruns on anaudit: falsetable with no source, an index and file-backed blobs.e187b83d2(RocksDB: "the renewed record must survive"; "an abandoned eviction must not queue the competing write's blob for reclamation"). On LMDB the first passes on base and with the fix; the others are skipped on LMDB.test:unit:resources: LMDB 3413 passing; RocksDB 4512 passing, with 2 load flakes (replicationSeqCursor,subscriptionReplay) that pass alone.test:unit:mainandtest:integration:allcould not finish locally next to 7 other suites, so CI ran them: all 28 unit and integration jobs (Linux, Bun, uWS, Windows) passed atffbd495, which has the same production code. At2a20c57, three unrelated flaky jobs were rerun.Table.evict()against real stores, indexes and blob files. A running server cannot order these interleavings deterministically.Test cases
ERR_BUSY. The blob file and its re-reference flag survive.collectRetainedFileIdsmakes this test fail.Refs #2861
🤖 Generated by Claude Code (Claude Opus); posted via @kriszyp.
Release target: v5.4 — the milestone of #2861; v5.3 has the same code, but no known need for a backport
Related PRs: #2155 overlaps (rewrites the same evict() version check and removal), #3375 overlaps (adds a replica branch inside evict()), 31 others independent
Complexity: complicated
Deferred findings
8c7a0517e9e70382minor data-integrity —resources/Table.ts— cleanup-scan HAS_BLOBS bypass untested; record gaining blobs before batch commit is reclaimed at staging — filed RocksDB eviction removes a resequenced record that reuses the version its caller read #33843e48438599eee7aeminor data-integrity —resources/Table.ts— post-commit eviction reclamation erases a concurrent writer's blob re-reference flag — filed Post-commit blob reclamation can clear an in-flight write's re-reference and unlink a live blob #3382Dispatch: task
harper-evict-rocksdb-version-gate-blob-commit· queued by unknown · ran by claude/opus/high · worker kzyp-xps-1Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; blocked=cursor-grok(failed); declined=cursor-composer,cursor-kimi,cursor-muse,muse; rounds=6; full=1 @ 2a20c57
Review-Attention: study ~15m (critical: Table.ts; decisions: per-caller-post-commit-reclaim, reclaim-on-commit-not-durability, version-gate-reused-version) @ 2a20c57