Repository navigation
Give the #1854 RocksDB oracle a checkpoint it cannot race - #2452
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request updates the RocksDB delete-index atomicity integration tests to use RocksDB checkpoints instead of opening the live database directory directly, preventing compaction races that lead to MANIFEST corruption. It introduces a /Snapshot/ endpoint to create checkpoints and adds an 'oracle proof' test to verify that the oracle reads from an immutable snapshot. Feedback on these changes suggests parsing the snapshot response body once instead of twice in the test file, and strictly validating the sequence parameter type in the snapshot resource to avoid overly permissive coercion by Number().
delete-index-atomicity-rocksdb.test.ts reddened Integration on main intermittently since it landed in #2400. Nothing under test is broken: the throw comes from the test's own oracle handle. A readOnly:true open maps to rocksdb::DB::OpenForReadOnly, which replays the MANIFEST into a file list and then opens those files holding no reference on any of them, so a compaction in the Harper process under test can unlink one inside that window (HarperFast/rocksdb-js#812). The oracle now reads a RocksDB checkpoint instead of the live directory. Nothing writes to a checkpoint, so the race cannot occur there at all, and createCheckpoint() flushes the memtable, which is what the previous /Flush/ call was for. One checkpoint also covers every column family at a single point in time, where the previous per-family opens each landed at a different instant. /Flush/ stays, now serving the new proof that the oracle really reads the checkpoint: a write flushed into the live directory's own SSTs that the oracle still cannot see can only mean the oracle is not reading it. Refs #1854, #2400, HarperFast/rocksdb-js#812 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179iGmr73TVDsn9HUiaSSHV
A snapshot-removal rejection in after() skipped teardownHarper() and leaked the Harper process; the removal now runs in a try/finally. refreshOracle() also asserts the fixture returned exactly the path it derives for this sequence, so a fixture regression that handed back a live directory fails closed instead of being opened — the proof test's inequality with rootPath alone would not catch every such path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179iGmr73TVDsn9HUiaSSHV
Parse the /Snapshot/ response once instead of twice, and reject a seq that is not a number: Number() coerced true, null and [] into a valid-looking 1 or 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179iGmr73TVDsn9HUiaSSHV
Retain the published checkpoint for teardown only after it matches the expected per-sequence path. Correct the proof commentary to distinguish frozen-handle behavior from the path assertion and avoid overstating cross-column-family atomicity. Co-Authored-By: Codex <noreply@openai.com>
kriszyp
force-pushed
the
test/rocksdb-oracle-checkpoint-snapshot
branch
from
September 2, 2026 05:34
ae29442 to
c4d173b
Compare
kriszyp
marked this pull request as ready for review
September 2, 2026 22:58
github-actions
Bot
requested review from
Ethan-Arrowood,
cb1kenobi and
heskew
September 2, 2026 22:58
Contributor
|
Reviewed; no blockers found. |
kriszyp
added a commit
that referenced
this pull request
Sep 4, 2026
…t the live database directory (#2502) * docs(tests): never read the server's live storage directory from a test The #1854 RocksDB-oracle suite reddened Integration 3/6 on main three times with `Corruption: IO error: No such file or directory ... 000021.sst`. The throw came from the test's own oracle handle, not from Harper: a `readOnly: true` open maps to `rocksdb::DB::OpenForReadOnly`, which replays the MANIFEST into a file list and then opens those files holding no reference on any of them, so a compaction in the process under test can unlink one inside that window. #2452 fixed that suite by reading a checkpoint instead; measured against a directory under continuous compaction, 273 of 3,804 live-directory opens failed and 0 of 5,804 checkpoint opens did. Nothing in the repo generalizes that. Write the rule and the checkpoint recipe into integrationTests/README.md so the next storage-oracle test does not rediscover it, and point the suite's header at the flake's tracking issue. Refs #2500, #2452, HarperFast/rocksdb-js#812 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkitGGCTzgAqogabbVhZZE * docs(tests): link the issue refs and require reclaiming the checkpoint Markdown files do not autolink issue references the way issue and PR bodies do, so the bare refs gave a README reader nothing to follow. The recipe also called a checkpoint cheap without saying it must be reclaimed: its hardlinks keep every SST it names alive through the live database's compactions, so a suite refreshing before each read would pin every historical file version for the whole run. The worked example already removes the previous checkpoint before taking the next; the README now says to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkitGGCTzgAqogabbVhZZE * docs(tests): name every directory Harper scans, not just database/ `resources/databases.ts` walks the `storage.path` root, the legacy schema directory, and every per-database `path` the config sets, adopting any child with a `CURRENT` and a `MANIFEST-*` as a database. "Beside `database/`" was therefore too narrow: a fixture that sets a per-database path and puts checkpoints under it would still have each one loaded as a database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkitGGCTzgAqogabbVhZZE --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Before #2429,
integrationTests/database/delete-index-atomicity-rocksdb.test.tsintermittently failed across the integration matrix because its independentreadOnly: trueoracle opened the live RocksDB directory while compaction could unlink an SST named by the MANIFEST. #2429 stopped the bleeding with a bounded error-signature retry; this rebased PR replaces that retry with an immutable checkpoint, removing the race by construction while preserving the raw-storage oracle and every Arm A / Arm B assertion./Snapshot/publishes the checkpoint throughcreateCheckpoint(), andopenDbi()refuses any live-directory fallback. Nothing writes to a checkpoint, so there is no missing-SST open race, error matcher, retry budget, or follow-up needed when HarperFast/rocksdb-js#812's secondary-mode open lands. Taking the checkpoint also flushes the memtable required by thisdisableWALfixture and captures all column families through one operation instead of separate read-only opens at unrelated instants; the suite is quiescent whenever it refreshes the oracle.refreshOracle()closes handles and removes the previous checkpoint before publishing the next, bounding pinned hardlinks to one. It retains a returned path for recursive teardown only when it exactly matches the expected per-sequence checkpoint, so a fixture regression fails before any handle opens it without risking removal of an invalid location. The target is derived fixture-side from the store and sits besidedatabase/, outside the directories scanned as databases.The snapshot proof pins the raw handle's
.pathto the checkpoint, proves that the checkpoint contains the preceding unflushed write, stays frozen after the live database advances, and is replaced by a refresh that sees the new write. The oracle remains a second@harperfast/rocksdb-jshandle decoding composite[indexedValue, primaryKey]keys directly; no Harper query surface substitutes for the index-side observation.For the human reviewer
Framing-Verdict: chosen-approach-sound.nulltombstones under the same key at a variable payload offset.hasLiveRecord()therefore uses a direct primary-key HTTP lookup while the index side stays raw; this is safe because the arms issue no client traffic betweenrefreshOracle()andfindPhantoms(). A future background workload in this fixture would require revisiting that assumption.Verification
Route: the changed integration test against live Harper/RocksDB instances, plus its client-surface companion anchor.
mainwith Deflake the #1854 RocksDB oracle: reopen when a live compaction removes an SST mid-open #2429,npm run buildpassed andnpm run test:integration -- "integrationTests/database/delete-index-atomicity-rocksdb.test.ts"passed three times at the final behavior: 9/9 each, 27/27 total. These runs also settle the reviewed question that the pinned rocksdb-js binding creates the checkpoint parent.npm run test:integration -- "integrationTests/resources/audit-false-delete-rollback.test.ts": 2/2 passed.npx prettier --check integrationTests/database/delete-index-atomicity-rocksdb.test.ts integrationTests/database/delete-index-atomicity-rocksdb/resources.js,npm run lint:required, andgit diff --check: passed.ItemFfail with the expected orphan while theaudit:truecontrols and oracle proofs stayed green; the rebase preserves those assertions unchanged.test:integration:all,test:unit:main, andtest:unit:resourceswere not rerun for this test-only rebase; CI remains the full-suite signal.Refs #1854, #2400, HarperFast/rocksdb-js#812
Complexity: medium
Review-Coverage: authored=codex; ran=cursor-composer,claude; adjudicated=domain; blocked=gemini(auth); declined=cursor-grok; rounds=2 @ c4d173b
Human-Review-Need: 3 (decisions: checkpoint-over-live-retry, snapshot-index-live-primary, replace-before-create) @ c4d173b