test(cluster): pin the mid-log txnlog tear test to the fail-stop quarantine semantics - #802
test(cluster): pin the mid-log txnlog tear test to the fail-stop quarantine semantics#802kriszyp wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the integration test txnlogTearReplication.test.mjs to align with a policy change where replication stops and reports a mid-log transaction-log tear instead of attempting to resync past it. The test is refactored to dynamically import constants from @harperfast/rocksdb-js, introduce robust row and payload helper functions, and assert that replication halts at the break, logs the diagnostic, and successfully resumes after the log is repaired. Feedback is provided regarding the breakDiagnostic helper, which uses hexadecimal formatting for offsets; this should be verified against the engine's actual log format to prevent test failures if decimal offsets are used instead.
… quarantine semantics The test asserted that replication resyncs past an unreadable mid-log frame and delivers everything behind it. That guarantee was reversed in harper#2087 to fail-stop/quarantine, and the test only started running on main when the rocksdb-js 2.8.0 bump (Sync Core b15fceb) un-skipped it, so the Cluster 6/6 job has been red since. The oracle now pins the decided policy: B holds exactly the rows whose frames precede the torn one, byte-exact; nothing behind the break replicates, including a write A acknowledges after the restart; and A's log reports the break with the offsets where it sits and where framing resumes. A missing CorruptFrameError export fails setup instead of skipping, and the frame-to-row mapping the oracle relies on is checked against each frame's bytes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhscgD5o72hSikQXoEzoR5
…indow, check A's table Review round 1 (Cursor Composer): a restarted node writes boot-time replay lines to the stopped incarnation's log dir before logging.root is repointed, so the break diagnostic is looked for in both; the negative window now spans a reconnect cycle; and A is checked after its restart to show only the log is torn, not the table. Narrating step comments dropped (Codex nit). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhscgD5o72hSikQXoEzoR5
…g tear Adjudication asked for the stated recovery path to be exercised, not only a negative window. Repairing the torn frame's length field and restarting A now has to deliver the write that was quarantined before, which also makes the earlier non-delivery attributable to the stop rather than to latency. Measured while adding it: the repair does not bring back the remainder of the torn transaction (r39..r59) -- B's cursor already sits at that transaction's version and the resume is exclusive of it -- so the test reports that count and leaves it unasserted instead of locking the loss in. Only a re-clone recovers those rows today. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhscgD5o72hSikQXoEzoR5
…ry direct operation Round 2 (Cursor Composer): the file/entry header sizes were hand-synced to rocksdb-js; they now come from its exported constants. Direct reads and inserts carry an AbortSignal so a node that accepts the socket and never answers fails the step instead of hanging the file until the suite timeout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhscgD5o72hSikQXoEzoR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhscgD5o72hSikQXoEzoR5
…on invariant holds everywhere Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhscgD5o72hSikQXoEzoR5
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
44a51f3 to
64efd4d
Compare
|
Reviewed; no blockers found. |
Cluster Integration Tests 6/6has been red on every Node leg sincefeat: Sync Coreb15fceb (2026-08-31), which bumped@harperfast/rocksdb-js2.7.1 → 2.8.0. 2.8.0 is the first release exportingCorruptFrameError(rocksdb-js#750), and that export wastxnlogTearReplication.test.mjs's skip gate: every earlier green run shows the test as# SKIP, so it ran onmainfor the first time that day and has failed every run since (B holds 39/60 rows). Nothing regressed. The test asserted that replication resyncs past an unreadable mid-log frame and delivers everything behind it; that guarantee was reversed in harper#2087 to fail-stop/quarantine ("stop at the break, and discard the transaction it truncated rather than commit part of it"), whose description already says that harper-pro#670's premise "no longer holds and needs re-scoping to the quarantine semantics". This re-scopes the test to that policy, in harper-pro only. The tear construction was right all along.Of the two framings the dispatch offered, neither held. Not (a), a regression in the resume path: it never existed on core
main—endIteratorOnCorruptFramelatches on anyRangeErrorand never callsnext()again, which the 2.8.0 reader's contract leaves to the caller. Not (b), a defective tear: the engine's own report confirms the unreadable shape (113 unreadable bytes = one 13-byte header plus the 100-byte payload, framing resuming at exactly the next frame). The old 60-row oracle could not hold under any design on the table either: the torn row's bytes are unreadable, and un-latching the guard delivers r40…r59 with r39 lost. harper-pro#669 (the readable tear shape) is open and untouched by this change; harper-pro#667 (cluster_statussurfacing) is open.The oracle now pins: B holds exactly the rows whose frames precede the torn one, byte-exact with id-specific delimited payloads, while A still holds every acknowledged row after its restart.
Nothing behind the break replicates, including a write A acknowledges after both nodes restart, over a bounded 10 s window. A's
hdb.logreports the break with the torn-frame and resumption offsets, keyed on the engine's message rather than its log level so it holds onmain(warn) and under harper#2087 (error).The repair arm first re-checks B after corrupt A has fully stopped, then repairs the frame and restarts A, and requires the quarantined write to arrive. That makes the repair a causal positive control rather than crediting it for delayed pre-repair delivery.
A missing
CorruptFrameErrorexport now fails setup instead of skipping, and the frame→row mapping is checked against every frame's bytes before tearing it. The header sizes come from the engine's exported constants and every direct operation is bounded by an AbortSignal. The review'sone-engine-onlyself-check refers to the RocksDB mention: the on-disk transaction-log framing andCorruptFrameErrorare rocksdb-js's, and there is no LMDB transaction-log file to tear, so no LMDB arm exists.Found while adding the repair control, and reported rather than asserted: after the repair, B receives the later transaction but never the remainder of the torn one (r39…r59). B committed the drained prefix with that transaction's version and its resume cursor is now exclusive of it, so a repaired log makes B consistent only for later transactions; the torn transaction comes back only with a re-clone, and nothing reports the gap. That is a replication resume defect adjacent to this test, left for its own issue.
Refs #669, #667, HarperFast/harper#2087, #670.
For the human reviewer
Framing-Verdict: chosen-approach-sound (7f3a95f8a97a). The planning review ran before implementation on a design note whose option set covered core resuming past the break, write-side prevention (rocksdb-js#748), a gate-only fix, deletion, waiting for #2087, and the reviewer-added Pro-triggered base copy after a reported break; each rejected on a stated fact.
CorruptFrameErrormessage in A'shdb.log, not a structured surface:cluster_statussurfacing (cluster_status reports a stream healthy after it has lost transaction-log entries — nothing consumes getCorruptFrameReports() #667) does not exist yet, so the log line is the only operator-visible signal to pin. When cluster_status reports a stream healthy after it has lost transaction-log entries — nothing consumes getCorruptFrameReports() #667 lands the assertion should move to it.CorruptFrameError. The dependency is pinned exactly at 2.8.0 and the diagnostic is part of the asserted contract; a silent skip is how this test spent a week "green" while asserting a reversed design. If the pin is ever loosened to a caret range an older engine makes this a red suite rather than a skip, which is the intended failure mode. Reversible.tearFramerequires exactly 60 frames and checks each frame's bytes against its row, so a future non-row entry in thedata/locallog fails setup with a message naming both counts rather than silently tearing the wrong frame. The per-frame identity check would catch most of that on its own; the count keeps the failure message plain.CorruptFrameErrorreport from either A incarnation because boot replay writes before the logging root moves. Alternative: require a new report only after B reconnects, which proves the live replication drain logged it. The current scope is the available operator signal; moving to a structuredcluster_statussignal remains harper-pro#667. A human may prefer the stricter live-stream attribution.data, but does not add a healthy second replicated database to distinguish per-stream fail-stop from peer-wide teardown. That extension has a different test topology and is intentionally left out of this rebase; a human may choose to make it a follow-up.Verification
Route (a): the existing integration test, re-scoped. All runs local with
distrebuilt first, rocksdb-js 2.8.0:Rebase verification (2026-09-03):
npm run test:integration -- integrationTests/cluster/txnlogTearReplication.test.mjs— passed 1 suite / 1 test in 75.97 s at64efd4d0; frame 39 was torn and B again reported 0/21 rows of the torn transaction's remainder after repair.npx prettier --check integrationTests/cluster/txnlogTearReplication.test.mjsandnpm run lint:requiredpassed.npm run buildemitteddistbut exited nonzero on pre-existing TypeScript errors outside this diff inanalytics/profile.ts,replication/replicationConnection.ts, andcore/server/threads/socketRouter.ts.harper-integration-test-run integrationTests/cluster/txnlogTearReplication.test.mjs— 10 standalone runs green across core 2e65550d7, harper#2087's head 181eac8da (checked out in the worktree'score, built, then restored), and this PR's pointer 9a3c75013; 29–47 s each (the old test failed at 39/60 after 104 s). Torn frame 39 = r39; A's log carriesat position 1126 of log 1 … valid framing resumes at 1197, 113 byte(s) unreadable(offsets in hex, as the engine prints them: 0x1126 = 4390); after the repair B holds 0/21 rows of the torn transaction's remainder, every time.Not vacuous: with the compiled
endIteratorOnCorruptFramepatched to callnext()again after aCorruptFrameErrorthat has a resync position, the test fails at the prefix check withexpected exactly 39 rows; missing 0 [], unexpected 20 [r40, …, r59]. Guard restored,distrebuilt.npm run test:integration:clusterlocally: not completed. The suite ran files concurrently on a shared fleet box carrying two other sessions' integration suites (load average 17); four unrelated files failed and six test children then hung with their nodes dead, so the run was killed. Re-run standalone afterwards:addNodeStartTimeandcopyFinalizeWedgeRecoverypass (contention flakes);cloneReadinessKeySet(QA-762, LMDB leader) andconnectedBitRestartChurn(QA-587) fail standalone on this box too, while every cluster shard other than 6/6 was green in the 2026-09-02 nightly, so they are box-specific and unrelated. TheCluster Integration Tests 6/6shard is the CI evidence for the rest.prettier --checkandoxlint --deny-warningson the file: clean.Complexity: complicated
Generated with Claude Fable 5.1.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NhscgD5o72hSikQXoEzoR5
Review-Coverage: authored=codex; ran=cursor-composer,gemini,claude; adjudicated=domain; declined=cursor-grok; rounds=2 @ 64efd4d
Human-Review-Need: 3 (decisions: partial-prefix-contract, repaired-remainder-contract, reporting-surface) @ 64efd4d