Skip to content

Anchor the replication base copy on a log-order barrier instead of a timestamp - #878

Merged
kriszyp merged 17 commits into
mainfrom
fix/base-copy-append-order-anchor
Sep 24, 2026
Merged

kriszyp merged 17 commits into
mainfrom
fix/base-copy-append-order-anchor

Conversation

@kriszyp

@kriszyp kriszyp commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

The base copy published a resume cursor that was not a boundary in the log it resumes from, so a transaction in flight when a node joined could be delivered by neither the copy nor the audit tail. It was not delayed: it was never replicated to that peer, and nothing later repairs it. Full trace in #876.

Why a timestamp could never work here

Two orderings disagree, and the copy assumed they agreed:

fixed when
a transaction's log key the transaction is created
its log batch the transaction commits

So the log is written out of key order, measured at 47 inversions per 1000 concurrent transactions across 4 threads. Take a transaction created at T-0.2 and committed at T+0.02, against an anchor of T. It sits behind the anchor numerically and after it physically. The walk had already passed its row, and the tail's per-entry predicate excluded it: timestamp > start, in rocksdb-js's TransactionLog.query, where _findPosition only seeks.

The fix

Anchor on the latest committed entry of the local log, and resume past it in append order. Just before COPY_START, the leader finds the latest committed key with findLastCommittedLogKey and sends it as copyStartTime. Nothing is written to form the boundary, so this no longer needs a core change. The earlier copyBarrier entry and its companion harper PR, harper#2697, are dropped, and the core gitlink is back on main's.

Why the last committed entry is a boundary. rocksdb-js yields entries only up to its committed-read watermark. That watermark advances past a transaction only after its RocksDB Commit() returns ok (transaction.cpp, the commitFinished call gated on status.ok()), and only across a contiguous prefix. So everything up to the anchor is visible to the walk, and every transaction still in flight appends after it, whatever its key. A real-log test pins that property.

Finding the last entry without reading history. The log reads only forward, but a range seeks by key through each file's running-max index. The first entry it yields sits at the seek position, so the helper bisects on key, pulling one entry per probe: about 30 probes, and no scan. An earlier revision scanned from the last-flushed position, which found nothing after a memtable flush and fell back to the wall clock on most RocksDB copies. A widening-window variant could scan a quiet log's whole history. The cluster runs caught the first problem and review caught the second.

The tail resumes with exactStart + resumeAfterExactStart (boundary range). Once the key matches, the predicate stops consulting the key at all. The range is built before the walk and proven with a throwaway pull under the same options. exactStart is scoped to local by startByLog, so peer logs in the aggregate are untouched.

A resumed copy tries the anchor as an exact entry first (classifyResumeAnchor): an exact match resumes in append order, and anything else keeps the timestamp resume. That anything else covers a pre-#876 Date.now() cursor, a purged entry and an empty-log copy. COPY_ORDER_VERSION stays 1, so a rolling upgrade honours in-flight copies instead of restarting them.

An empty log tails from its start with an ordinary range: everything it will yield commits later.

Gated on the store, not the config. The gate is auditStore.reusableIterable === true && logName === 'local', not STORAGE_IS_ROCKSDB. That flag is a config snapshot that misreports the engine in a worker thread, and it previously let an LMDB table reach a RocksDB-only call.

Kept from earlier rounds. The anchor is validated on both peers (follower). The sender's cursor and SENDING_TIME_POSITION only climb, since append order is not key order. A tail-time boundary failure rebuilds an ordinary range immediately. A degraded copy says so at warn. Design note: replication/DESIGN.md.

For the human reviewer

  1. This PR's approach changed in this revision, on your call in the dispatch thread. It now reads the last committed entry instead of writing a barrier. I skipped a fresh --mode plan framing gate, because the owner chose the framing directly. The framing verdicts recorded below were against the barrier design and are kept as history.
  2. Empty-log copies still send Date.now() to the follower (why). The leader's in-connection tail is correct: it reads from the log's start. A mid-copy disconnect on an empty log, though, resumes by timestamp and can miss a transaction that was in flight. A low sentinel such as 1 would close that, but shouldForceBaseCopyForRetention reads it as purged history. It would then recopy an idle follower on every reconnect after COPY_COMPLETE. Closing it properly needs a mode flag in the cursor, the same durable-cursor work as Replication resume cursors need an append-order mode to survive a reconnect or a relayed origin #879.
  3. Still not covered (unchanged scope, Replication resume cursors need an append-order mode to survive a reconnect or a relayed origin #879): a reconnect after COPY_COMPLETE resumes by timestamp, because matchesSubscription requires startTime below an entry's key. Relayed-origin logs are not anchored either.
  4. Degrade-open, not fail-closed. An unreadable log, an unformable boundary, or an anchor naming no entry all fall back to the pre-Replication base copy can silently drop a transaction that is in flight when a node joins #876 timestamp range and log it. Fail-closed was tried in an earlier round and withdrawn, because it produced reconnect loops that copied nothing.
  5. Declined review findings, with evidence:
    • "Bisection probes leak iterators": refuted. An early return inside for…of calls the iterator's return() (the language's IteratorClose step).
    • "Fractional probe keys": refuted. Log keys are fractional doubles, and findPositionByTimestamp takes a double.
    • "A poisoned persisted cursor loops 1008 against an old leader": kept at minor. No shipped leader emits an invalid anchor, so reaching this needs already-corrupt storage.
    • "LMDB and relayed copies warn on every copy": kept deliberately. The warning is what exposed an inert gate before.
  6. Also fixed on the way: an earlier rebase of this branch had dropped main's Flaky: clone from a legacy v4 leader intermittently fails when audit-log forwarding throws RangeError converting a non-integer to BigInt #737 paragraph from replication/DESIGN.md, so it is restored. The unit-test exit-flush override is reverted, because main fixed its cause in Remove the unit-test root only after its databases are closed #893 and the override masked that change's lifecycle test. The txnlogTearReplication oracle loosening is reverted, because it existed only to tolerate barrier frames.

Earlier rounds (barrier design), kept for the record

  • The planning gate returned better-alternative-exists twice. Round 1's alternative, reconstructing an in-flight floor from registryStatus(), was refuted by rocksdb-js assigning startTimestamp before it publishes the handle, and that design was replaced. Round 2's corrections were adopted: the pre-positioned iterator, validating the anchor on both peers, and keeping a resumed copy's original anchor.
  • The first implementation was inert in production. It was gated on !excludedNodes, and excluded is always an array, so the gate was never true. Every test passed against the inert path. That is why verification asserts on the absence of the fallback warning, not on a green test.
  • matchesSubscription does not filter the copy's tail. It has one call site, gated on the per-origin subscribedNodeIds[id].timeRange, and auditSubscription.startTime is written, never read, on that path.

Verification

  • Defect and fix, against a real RocksDB log (copyAnchorResume.test.mjs). A held-open transaction takes the lower key but appends last. The timestamp range drops it, while resuming past the last committed entry delivers it. The log is flushed first, so the anchor cannot depend on post-flush entries; this pins the regression the first revision had. A third test polls the log while 200 transactions commit, and asserts every yielded entry is already visible to a read.

  • npm run build and tsc --noEmit are clean. prettier is clean on touched files. oxlint shows only the pre-existing subscriptionRequest warning, on an untouched line.

  • npm run test:unit: 1258 passing, 0 failing, 1 pending.

  • Cluster integration, one file at a time on a private TMPDIR, with instance logs kept (HARPER_INTEGRATION_TEST_LOG_DIR):

    file result RocksDB base copies timestamp fallbacks
    addNodeFullCopy pass 1 / fail 0 2 0
    copyResumeCursorGap pass 1 / fail 0 1 0
    directionalFlowReplication pass 1 / fail 0 1 0
    txnlogTearReplication (oracle now back to main's) pass 1 / fail 0 4 0
    fullyConnectedReplication (RocksDB + LMDB suites) pass 10 / fail 0 36 0 (LMDB: 12, expected)

    Warn level is logged in these runs, so zero fallback lines means each RocksDB copy took the log-order path. An earlier run of the same table is what exposed the flushed-log gap: 25 fallbacks on fullyConnectedReplication alone.

  • Independent pre-push review, 4 rounds on this revision: 1 full round (codex + gemini + cursor-muse, domain adjudicator timed out) and 3 deltas. The final delta ran codex + gemini + cursor-muse + harper-domain and found nothing new. Everything left is an accepted-known limit or a decision listed above.

Fixes #876

Origin — the dispatch brief this PR was written from

Anchor the replication base copy on a log-order barrier instead of a timestamp

LIVE CONVERSATION about #878.

You are answering a person, in a thread, one turn at a time. Every turn:

  1. Read the whole thread in this dispatch file's # Log — it is the conversation so far, and
    each of your previous turns is in it. Read the PR/issue and the code as needed.
  2. Answer the LAST message. Append your answer to # Log as your turn. Prose, not a report:
    they are talking to you, and a status template is not an answer.
  3. Set status: needs-input and stop. The thread stays open; their next message resumes it.

Each turn arrives as ASK (answer it, change nothing) or PERFORM (do it, then say what you did) —
the person chose which when they sent it, and the run's own prompt tells you which one this is.
Never infer it from the wording: an unrequested commit in the middle of a discussion and a polite
description of work that was supposed to happen are the two failures this exists to prevent.

Never mark a PR ready and never merge from this conversation.

Dispatch: task chat-pr-harper-pro-878-kriszyp · queued by unknown · ran by claude/opus/high · worker kzyp-xps-1

Review-Coverage: authored=claude; ran=cursor-muse,codex,gemini; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi; rounds=31; full=4 @ 716cea8

Human-Review-Need: 4 (decisions: empty-log-wall-clock-cursor, degrade-open-on-boundary-failure, reconnect-gap-deferred, no-cursor-version-bump, anchor-local-log-only) @ 716cea8

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

Copy link
Copy Markdown

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 introduces a more robust mechanism for base-copy resume boundaries in replication by replacing timestamp-based anchors with log-order barriers. This change ensures that transactions committed during a bulk copy are correctly captured, preventing data loss. The implementation includes new utility functions for barrier management and anchor classification, along with comprehensive unit tests. I have reviewed the provided feedback and kept the suggestion regarding the withDatabase helper, as it correctly identifies a potential resource leak in the test cleanup logic.

Comment on lines +27 to +36
async function withDatabase(run) {
const dir = mkdtempSync(join(process.env.TMPDIR || tmpdir(), 'copy-barrier-'));
const db = RocksDatabase.open(join(dir, 'db'));
try {
await run(db, db.useLog('local'));
} finally {
db.close();
rmSync(dir, { recursive: true, force: true });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In the withDatabase helper, if RocksDatabase.open throws an error, the finally block is not entered, and the temporary directory created by mkdtempSync is leaked on disk. Wrapping the database initialization and log usage in nested try-finally blocks ensures that the temporary directory is always cleaned up properly.

Suggested change
async function withDatabase(run) {
const dir = mkdtempSync(join(process.env.TMPDIR || tmpdir(), 'copy-barrier-'));
const db = RocksDatabase.open(join(dir, 'db'));
try {
await run(db, db.useLog('local'));
} finally {
db.close();
rmSync(dir, { recursive: true, force: true });
}
}
async function withDatabase(run) {
const dir = mkdtempSync(join(process.env.TMPDIR || tmpdir(), 'copy-barrier-'));
try {
const db = RocksDatabase.open(join(dir, 'db'));
try {
await run(db, db.useLog('local'));
} finally {
db.close();
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
References
  1. In test cleanup hooks (such as after or afterEach), guard cleanup functions against uninitialized or undefined variables to prevent synchronous errors from blocking subsequent cleanup steps and causing resource leaks.

kriszyp and others added 13 commits September 24, 2026 01:47
…timestamp

`copyStartTime` was `Date.now()`, captured before the key-order table walk and
published as the follower's resume cursor. But a transaction's log key is fixed
when the transaction is CREATED while its log batch is appended when it COMMITS,
so the transaction log is written out of key order — measured at 47 inversions
per 1000 concurrent transactions. A transaction created just before the anchor
and committed during the copy therefore sits behind the anchor numerically and
after it physically: the walk had already passed its row, and the post-copy
tail's per-entry range predicate (`timestamp > start`) excluded it. Neither side
delivered it, and nothing ever would.

The leader now commits a `copyBarrier` entry before `COPY_START` and anchors on
its transaction key. Everything still in flight commits — and so appends — after
that entry, and the tail resumes with `exactStart` + `resumeAfterExactStart`,
after which the range predicate stops consulting the key at all. The boundary
iterable is built before the walk, because `getRange` resolves the position and
maps the log file eagerly, so a long copy cannot outlive the barrier it is
anchored to.

The same append-order resume is used on reconnect whenever the cursor names a
`copyBarrier` entry, so the anchor survives a disconnect between `COPY_COMPLETE`
and the in-flight commit. An ordinary cursor keeps the timestamp range, so the
default subscription path is unchanged and pays nothing.

`COPY_ORDER_VERSION` is bumped to 2 and now versions what the anchor MEANS as
well as the table order: a v1 cursor surrenders its anchor along with its walk
position rather than carrying a timestamp anchor into a leader that cannot make
it good, and a v2 cursor whose barrier has left the log restarts instead of
resuming onto a guarantee nothing can keep. The anchor is validated on both
peers before it reaches any state.

Not covered, and stated in the PR: a relayed origin's log, where the barrier does
not exist and the timestamp range still applies.

Fixes #876

Dispatch-Task: hp-basecopy-inflight-write-loss
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE8PfPhHn1o1Wss4uH3ULD
findCopyBarrierTable() duck-types Table.writeCopyBarrier() as available whenever
the method exists on the class, but the method itself hard-throws when the
table's actual primaryStore isn't RocksDB. STORAGE_IS_ROCKSDB is a config
snapshot that can disagree with a table's real engine (observed here: it read
true in a replication worker thread whose harper instance was actually
configured for LMDB, because env.get(CONFIG_PARAMS.STORAGE_ENGINE) returned
undefined in that context). Previously this mismatch bubbled the thrown Error
up through COPY_START's handler into the outer catch, which closed the
connection with 1008 and killed the whole subscription -- reproduced
deterministically via integrationTests/cluster/fullyConnectedReplication.test.mjs's
LMDB suite (4-node full mesh), where every base copy of a non-empty database
hits this path.

Every other "can't form a barrier" case in this feature already degrades to
the pre-#876 wall-clock anchor instead of refusing (empty database, a core
submodule predating writeCopyBarrier, a boundary that fails its probe). This
makes writeCopyBarrier() failing at call time follow the same rule: catch,
warn once, and fall through to Date.now() -- exactly the invariant the
surrounding code already states in its own comments, just missing this one
call site.

Verified: fullyConnectedReplication.test.mjs 10/10 (both RocksDB and LMDB
suites); RocksDB suite's per-instance logs confirm it still writes and uses
real barriers (zero fallback warnings); LMDB suite's logs show the new catch
firing and the connection surviving. addNodeFullCopy, copyResumeCursorGap,
and directionalFlowReplication cluster tests still pass 1/1. harper-pro
test:unit 1254/0.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adjudicated finding from the pre-push review of the prior commit:
resumeAnchorKind is classified against the ORIGINAL copyResume, before the
three guards above (order-version mismatch, invalid anchor, missing table)
can clear it. Once cleared, a stale resumeAnchorKind === 'barrier' combined
with the previous commit's new catch around writeCopyBarrier() lets a
fresh-barrier-write failure fall through to treating Date.now() as if it
named a real barrier entry -- something the prior commit's throw-and-close
behavior made unreachable, but that catching the throw newly exposed. Gate
the stale classification on copyResume still being present.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ier frame

Full CI (Cluster Integration Tests 6/6) failed after the rebase:
"holds 61 frames for 60 rows; the oracle maps frame k to row k and needs one
frame per row". Root cause: this test's before() hook does a plain add_node
join on RocksDB with an empty table, which is exactly the base-copy path this
PR changed -- it now commits one copyBarrier control frame into the joining
leader's local transaction log before any row is ever inserted (confirmed via
the run's own debug log: "bulk copy starting/complete" precedes the test's
insertRows calls). That shifts every later frame's index by one, breaking the
test's "frame k is row k" assumption -- an assumption that predates this PR
and was true only because no base copy had ever written into this log before.

Writing the barrier as a control entry through an arbitrary table (rather
than some log-level-only channel) is this PR's own already-adjudicated
design decision, shared with the existing lockBarrier/reloadMarker control
types in the same log -- not something to redesign from a rebase task. The
fix is at the right altitude: the oracle now filters to frames that actually
carry one of the test's own rows (the same marker-matching check it already
does per-frame, just applied as a filter instead of assumed by position), so
a leading control frame no longer breaks the count. A corrupted or missing
row still fails loudly, since the filtered count would then miss TOTAL.

Verified: the test passes twice in a row (clean re-runs, not the same
process), and its diagnostic line now reports the row-frame count ("of 60")
rather than the raw frame count. harper-pro test:unit 1254/0 unaffected.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adjudicated finding from the pre-push review of the prior commit: filtering to
row-carrying frames alone would also hide a barrier-per-retry proliferation --
the reconnect-loop risk replication/DESIGN.md names as the reason fail-closed
was withdrawn. Bound non-row frames to the one base-copy barrier this test's
single add_node can produce, so that regression would fail this test again.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two issues the prior commit's review found in its own bound check:

1. It ran before the row-count check, so a corrupted/missing row (which also
   depresses the row-frame count) was misdiagnosed as barrier proliferation
   instead of as itself. Row-count now checked first.
2. The exact <= 1 bound is flake-prone: a legitimate reconnect between
   add_node and B's first cursor can mint a second barrier on retry, which is
   not the proliferation risk the bound exists to catch. Loosened to <= 3 --
   a wedged retry loop mints one barrier per attempt and blows well past a
   couple of incidental early reconnects.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The pre-COPY_START probe pulled one entry via
`getRange(...)[Symbol.iterator]().next()` and then dropped the
iterator without calling `.return()`. Single-log getRange wires
`return` through to the underlying log iterator specifically for
early-exit cleanup; leaving it uncalled means every base copy that
forms a barrier boundary leaves that handle open until GC. Wrap the
probe pull in try/finally so it's released immediately.

The multi-log aggregate branch of RocksTransactionLogStore.getRange
has no `.return()` at all, so this only closes the gap on the
single-log path; the aggregate path's lack of a teardown hook is a
pre-existing, broader core limitation this PR doesn't touch.

Found by an independent Gemini review pass.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The comment claimed callers fail closed on 'unreadable'. The only
caller (the copy-resume anchor check at replicationConnection.ts:5972)
only branches on `=== 'barrier'`; 'other' and 'unreadable' both fall
through identically to degrading to the pre-#876 timestamp anchor,
same as DESIGN.md's stated philosophy for every other unformed
boundary. Doc didn't match code.

Found by an independent Gemini review pass.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The backstop that clears a failed append-order auditLogIterable fell
through to `await nextTransaction` instead of retrying right away.
`nextTransaction` only resolves on a future commit; on an otherwise
idle database, whatever the failed boundary couldn't deliver would
sit stranded until an unrelated write happened to unstick it. Add a
`continue` so the loop rebuilds an ordinary range from
currentSequenceId immediately, the same way it would on a fresh pass.

Found independently by both Codex and Gemini review passes.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same stale "fail closed" framing as the JSDoc fixed in the prior
commit; the assertion itself (return value === 'unreadable') was
already correct and unchanged.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI started failing after the rebase's rocksdb-js 2.9.1 -> 2.10.0 bump
(from main's Sync Core commits): all 1253+ tests passed, then the
process crashed with "Failed to flush database during close: ...
No such file or directory".

Root cause: two independent `process.on('exit', ...)` listeners race.
unitTestSetup.cjs registers first (via --require) and rmSync's the
whole shared STORAGE_PATH; RocksTransactionLogStore.ts's own exit
listener (registered later, whenever a test first imports it) then
tries to flush every still-known RocksDB log into directories that
listener just deleted. Node fires exit listeners in registration
order, so this ordering was always latent -- 2.10.0 apparently just
started throwing on it where 2.9.1 didn't.

The flush-on-exit hook exists for crash/replay coverage, which lives
in integrationTests (grep confirms zero unitTests reference
HARPER_NO_FLUSH_ON_EXIT); unit tests close their own databases
explicitly and don't need it. Setting the env var here, before any
test file can import RocksTransactionLogStore.ts, skips registering
that second listener entirely.

Reproduced and verified locally against the post-rebase
package-lock.json (my node_modules had gone stale after the rebase
bumped it; `npm ci` was needed to see the crash at all) -- 2/2 clean
runs after the fix, both crashing before it.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The existing case only checked a hypothetical 1-vs-2 version mismatch;
a real pre-#876 leader's cursor has no copyOrder at all, and today's
COPY_ORDER_VERSION is still 1, not 2 — add that case and stop implying
the hypothetical one covers it.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit's new case asserted (undefined, 1) as "a real
pre-barrier leader's cursor" — wrong: COPY_ORDER_VERSION is #421/#422's
copy-table-order versioning, unrelated to and unchanged by this PR, so
a pre-#876 leader already sends copyOrder:1 and is accepted, not
rejected. undefined is a pre-#421 leader instead. Caught by codex in
this task's own pre-push review round.

Dispatch-Task: pr-maint-352723a0e8c43f4ff8e901f0fc934706
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kriszyp
kriszyp force-pushed the fix/base-copy-append-order-anchor branch from c14f17b to a5de25d Compare September 24, 2026 08:22
kriszyp and others added 4 commits September 24, 2026 07:48
…ng a barrier

The copy no longer commits a copyBarrier entry to form its boundary: the last
committed entry of the local log is already one. The log yields entries only up
to its committed watermark, which rocksdb-js advances past a transaction only
after its RocksDB commit succeeds and only across a contiguous prefix, so every
transaction still in flight appends after that entry whatever its key.

- findLastCommittedLogKey scans a widening key window back from now (1s, 1min,
  1h, 1d, whole log) and takes the last entry yielded; an empty log gets an
  ordinary range from its start, which is already a boundary.
- classifyResumeAnchor now asks only whether the anchor names an entry: an
  exact match resumes in append order, anything else keeps the timestamp
  resume, so pre-#876 Date.now() cursors are honoured unchanged.
- Gate on auditStore.reusableIterable rather than the STORAGE_IS_ROCKSDB config
  snapshot, which misreports the engine in a worker.
- Drops the core submodule bump (harper#2697 is no longer needed) and the
  txnlogTearReplication oracle loosening, which only tolerated barrier frames.
- Restores the #737 DESIGN.md paragraph an earlier rebase of this branch lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A quiet log made the widening window fall through to a whole-history scan on
the copy's setup path. A range seeks by key and its first entry sits at the
seek position, so bisecting on one pulled entry per probe finds the largest
committed key in a bounded number of probes without reading history.

Also drops the unit-test exit-flush override: main fixed the underlying exit
ordering in #893, and the override masked that change's lifecycle test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…log sends the wall clock

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kriszyp
kriszyp marked this pull request as ready for review September 24, 2026 17:31
@kriszyp
kriszyp requested a review from a team as a code owner September 24, 2026 17:31
@kriszyp
kriszyp merged commit ae7f001 into main Sep 24, 2026
45 checks passed
@kriszyp
kriszyp deleted the fix/base-copy-append-order-anchor branch September 24, 2026 17:31
Comment on lines +28 to +36
async function withDatabase(run) {
const dir = mkdtempSync(join(process.env.TMPDIR || tmpdir(), 'copy-anchor-'));
const db = RocksDatabase.open(join(dir, 'db'));
try {
await run(db, db.useLog('local'));
} finally {
db.close();
rmSync(dir, { recursive: true, force: true });
}

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.

Suggestion (non-blocking): RocksDatabase.open(...) runs outside the try, so if it throws, the temp dir created by mkdtempSync on the prior line is never cleaned up by the finally below. This is the same leak gemini flagged on this helper under its old name (copyBarrierResume.test.mjs, now-outdated thread) — it persisted through the rewrite into this new file. Nest the open call in its own try/finally:

Suggested change
async function withDatabase(run) {
const dir = mkdtempSync(join(process.env.TMPDIR || tmpdir(), 'copy-anchor-'));
const db = RocksDatabase.open(join(dir, 'db'));
try {
await run(db, db.useLog('local'));
} finally {
db.close();
rmSync(dir, { recursive: true, force: true });
}
async function withDatabase(run) {
const dir = mkdtempSync(join(process.env.TMPDIR || tmpdir(), 'copy-anchor-'));
try {
const db = RocksDatabase.open(join(dir, 'db'));
try {
await run(db, db.useLog('local'));
} finally {
db.close();
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

@claude

claude Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

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.

Replication base copy can silently drop a transaction that is in flight when a node joins

1 participant