test(cluster): prove mid-log txnlog tear recovery end-to-end - #670
test(cluster): prove mid-log txnlog tear recovery end-to-end#670kriszyp wants to merge 3 commits into
Conversation
Two-node replication over a genuinely damaged log on disk: B's resume cursor is established, B goes offline, rows are written to A and acknowledged, a frame near the end of A's local log is torn, and both nodes come back. B must read through the break to the last row. Covers harper#2016 and harper#2063, whose fix PRs (harper#2087, rocksdb-js#750) are otherwise proven only against synthetic iterators and hand-built buffers. Measured on this harness: released rocksdb-js 2.7.0 leaves B at 39/60 rows, the #750 build converges at 60/60. Skipped unless the engine reports resyncPosition. The readable tear shape is deliberately not covered here: the torn frame is still yielded and its undecodable payload wedges the receiver (harper-pro#669). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request adds a new integration test, txnlogTearReplication.test.mjs, to verify that replication resyncs past a mid-log transaction-log tear instead of stalling. The review feedback suggests wrapping individual cleanup steps in separate try-catch blocks within the after hook to ensure that a failure in one step does not prevent subsequent cleanup steps from executing.
|
Delta review — commits 0c1e2fe + a42e810 (since last review 2026-08-07) One file changed ( What the two commits do0c1e2fe (Address txnlog tear test review feedback)
a42e810 (Log txnlog tear cleanup failures) Findings1. Zero-loss assertion — decision confirmation requested (advisory)The PR description explicitly flags this as the key open decision: "the upstream framing fix says the broken frame itself can be lost, while this harness previously measured 60/60 on the fixed engine and the Aug 21 review asks to pin that observed zero-loss behavior." The final assertion is: ok(
missing.length === 0,
`resync should preserve every acknowledged row, but B is missing ${missing.length}: ...`
);This includes the row in the torn frame — the one whose payload bytes the test rewrites. The rocksdb-js #750 PR description says the engine reports where framing resumes after a tear (so entries after the break are recovered), but does not guarantee the torn frame's own payload is reproduced. If the implementation skips the broken frame's payload (which would be semantically correct for a framing-recovery-only fix), this assertion fails on a correctly-fixed engine. The comment added in 0c1e2fe reads "including the row in the torn frame" and the author's measured result is 60/60. That is consistent with the engine performing a full-copy resync for the torn segment — which would recover the torn frame's row from the receiving node's perspective. This is a legitimate design decision — the test is pinning a measured, desired behavior — but it should receive explicit human reviewer acknowledgement before merge, because it encodes an assumption about the engine's resync mechanism that is not obvious from the framing-fix spec alone. 2.
|
|
The engine gate holds up: CorruptFrameError is merged and exported on rocksdb-js main, so this arms itself on the next pin bump, and the header constants match transaction_log_file.h. The measured 39/60 on the released engine is arithmetically consistent with the tear position (60 minus the 20 trailing frames minus the torn one), which corroborates the placement. Both decision points: accept; the export is the capability, and covering the unreadable half now with #669 tracking the readable half is right, though #669 should stay the blocker on calling #2016/#2063 closed. Two asks:
Nit: the doc comment says the gate is resyncPosition; the code gates on CorruptFrameError. Lavinia, via Claude (I reviewed this before posting) |
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
|
Addressed the Aug 21 review asks in 0c1e2fe/a42e8105:
Local checks: lint and 626 unit tests pass. The focused integration entry is capability-skipped on the pinned rocksdb-js 2.7.0, as expected; the full cluster gate was attempted but could not complete on this shared worker because the loopback pool was occupied. |
End-to-end coverage for harper#2016 — Mid-log corrupt transaction-log frame silently truncates replay and replication — acknowledged writes lost and harper#2063 — Corrupt-log containment in RocksTransactionLogStore.getRange is per-drain.
Why this exists
Both fix PRs — harper#2087 and rocksdb-js#750 — state the same coverage ceiling in their own descriptions: every test drives synthetic iterators or hand-built buffers, and nothing reopens a genuinely damaged log on disk and watches a real consumer replicate past the break. This is that test.
What it does
Two nodes. B's resume cursor is established on a healthy stream (10 rows), B goes offline, 50 more rows are written to A and acknowledged, then A is stopped and a frame 20 frames from the end of its
data/locallog is torn on disk. Both nodes come back and B resumes from its pre-tear cursor.The oracle is the last row written, not a count: a stream that stopped at the tear never delivers the tail, however many rows it managed first.
Measured
That is the fails-on-base check: the test reproduces the defect on today's released engine and passes on the fixed one, so it is not vacuous.
Worth a reviewer's attention
It skips on a released engine. The gate is the
CorruptFrameErrorexport, which only the fixed engine has. Until the rocksdb-js bump lands, this is a skip in CI — deliberate, so it can merge ahead of the engine, but it means CI green here is not evidence the test runs.HARPER_TXNLOG_TEAR_FORCE=1runs it anyway, which is how the base column above was produced.It tears the frame by rewriting the declared length, not by truncating. Framing is walked from the file header using the 13-byte header layout, duplicated as constants from rocksdb-js's
transaction_log_file.h. If that layout changes, this test breaks at the magic-number assertion rather than silently testing nothing.It covers the unreadable tear shape only, and that is the narrower half. A partial append usually leaves a readable torn frame — the declared length overruns into the next frame but still fits the file — which the reader yields as a well-formed entry with a garbage payload. Framing recovery cannot help there. Measured on this same harness, that shape leaves B at 39/60 even on a fixed engine, wedged on repeated decode failures rather than on the framing break. Filed as #669 — A readable torn transaction-log frame poisons the replication stream. Reviewers should read that before treating #2016/#2063 as closed by the two fix PRs.
Independent pre-push review ran at the current head. Claude and Gemini completed the final delta review. The review retained a semantics question: the upstream framing fix says the broken frame itself can be lost, while this harness previously measured 60/60 on the fixed engine and the Aug 21 review asks to pin that observed zero-loss behavior.
For the human reviewer
decision — tear-shape-scope — cover the unreadable shape and file the readable one vs block this test until the readable shape also passes — reversible; the alternative leaves the proven half of the fix unproven while #669 is designed
decision — engine-gate-by-export — gate on the
CorruptFrameErrorexport vs a version comparison on the installed engine — reversible; the export is the actual capability, a version string is a proxy that a local link or prerelease breaksGenerated with Claude Opus 5.
🤖 Generated with Claude Code
Review-Coverage: authored=codex; ran=claude,gemini; declined=cursor-grok,cursor-composer,domain; rounds=2 @ a42e810
Human-Review-Need: 4 @ a42e810