Skip to content

Give the #1854 RocksDB oracle a checkpoint it cannot race - #2452

Merged
kriszyp merged 5 commits into
mainfrom
test/rocksdb-oracle-checkpoint-snapshot
Sep 2, 2026
Merged

kriszyp merged 5 commits into
mainfrom
test/rocksdb-oracle-checkpoint-snapshot

Conversation

@kriszyp

@kriszyp kriszyp commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Before #2429, integrationTests/database/delete-index-atomicity-rocksdb.test.ts intermittently failed across the integration matrix because its independent readOnly: true oracle 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 through createCheckpoint(), and openDbi() 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 this disableWAL fixture 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 beside database/, outside the directories scanned as databases.

The snapshot proof pins the raw handle's .path to 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-js handle decoding composite [indexedValue, primaryKey] keys directly; no Harper query surface substitutes for the index-side observation.

For the human reviewer

  1. Checkpoint instead of the now-merged live-open retry. This rebase deliberately removes Deflake the #1854 RocksDB oracle: reopen when a live compaction removes an SST mid-open #2429's bounded classify-and-reopen loop rather than stacking both mechanisms. Keeping Deflake the #1854 RocksDB oracle: reopen when a live compaction removes an SST mid-open #2429 is smaller, and waiting for readOnly:true open races a live writer's compaction and reports a healthy database as corrupt; no OpenAsSecondary alternative exists rocksdb-js#812 would avoid fixture checkpoint scaffolding; the checkpoint is chosen because it eliminates the race and error-shape coupling now, with no measurable suite-time cost. Reversal is localized to the fixture route and oracle helpers. Design framing cleared before implementation: Framing-Verdict: chosen-approach-sound.
  2. Checkpointed index entries compared with live HTTP primary liveness. Decoding the primary column family from the same checkpoint would avoid split-time observation, but audited deletes store null tombstones 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 between refreshOracle() and findPhantoms(). A future background workload in this fixture would require revisiting that assumption.
  3. Remove the old checkpoint before creating the new one. Publishing first would preserve the last snapshot if refresh fails and improve failure forensics; removing first keeps hardlinked SSTs bounded to one checkpoint. The harness uses a fresh temporary data root per run, and the choice is easy to reverse if debugging value outweighs the disk bound.

Verification

Route: the changed integration test against live Harper/RocksDB instances, plus its client-surface companion anchor.

  • The original stress campaign reproduced the base failure twice in 36 runs at 12-way parallelism, then completed 144/144 at the reproducing load, 36/36 at 6-way, and 12/12 at 3-way with the checkpoint implementation: 192 runs, zero race signatures, with 36-run wall time effectively unchanged (114 seconds on base versus 115–121 seconds with checkpoints).
  • After rebasing onto main with Deflake the #1854 RocksDB oracle: reopen when a live compaction removes an SST mid-open #2429, npm run build passed and npm 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, and git diff --check: passed.
  • The earlier fix: thread {transaction} into removeEntry for audit:false table deletes #1869 sensitivity run remains applicable: reverting the transaction option made Arm A and Arm B on ItemF fail with the expected orphan while the audit:true controls and oracle proofs stayed green; the rebase preserves those assertions unchanged.
  • test:integration:all, test:unit:main, and test:unit:resources were 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

@kriszyp kriszyp added this to the v5.3 milestone Sep 1, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Comment thread integrationTests/database/delete-index-atomicity-rocksdb.test.ts Outdated
Comment thread integrationTests/database/delete-index-atomicity-rocksdb/resources.js Outdated
kriszyp and others added 4 commits September 1, 2026 22:48
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
kriszyp marked this pull request as ready for review September 2, 2026 22:58
@kriszyp
kriszyp merged commit 114c078 into main Sep 2, 2026
45 checks passed
@kriszyp
kriszyp deleted the test/rocksdb-oracle-checkpoint-snapshot branch September 2, 2026 22:58
@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant