Skip to content

copyApply: snapshot non-system base-copy rows without audit entries (part of #480) - #486

Merged
kriszyp merged 5 commits into
mainfrom
kris/copy-apply-snapshot
Jun 25, 2026
Merged

kriszyp merged 5 commits into
mainfrom
kris/copy-apply-snapshot

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Receive-side half of copyApply (paired with harper#1479). Base-copy frames are applied as current-state snapshots — no audit/transaction-log entry, no out-of-order resequencing — instead of being re-fabricated as transactions. Removes the O(n) keyed audit-dedup spin and the per-peer transaction-log amplification when copying an analytics-laden table (#480).

Purpose

Copying a table whose rows predate transaction-log retention makes every applied row's keyed dedup miss and scan to end-of-log (one worker ~100% CPU, no forward progress), and re-fabricating copied rows as transactions floods the per-peer logs that other peers then copy. See #480.

Design — please focus review here

  • C2 gate (event.isCopyApply = messageIsCopyFrame && auditRecord.version < copyModeStartTime): only rows older than copyStartTime are snapshotted. The post-copy audit replay resumes from copyStartTime and re-delivers every row >= copyStartTime, so those keep real audit entries and the redelivery dedups (a commutative patch would otherwise double-apply). Older rows are never re-delivered → safe to snapshot. RocksDB-only (core gates on isRocksDB).
  • Latched copy-frame status (messageIsCopyFrame, once per WS body): the decode loop awaits (waitForDrain/setImmediate) and ws does not serialize async handlers, so a later COPY_COMPLETE could flip copyCompleteReceived mid-body and drop trailing rows to the audited path.
  • Durability (the load-bearing part): copied DBs run WAL-off and these rows carry no transaction-log entry, so the persisted resume cursor and the [seq] watermark must only advance behind an explicit RocksDB flush (memtable → SST):
    • flushDurableCopyCursor flushes on a replication_copyCursorFlush{Bytes,IntervalMs} cadence (64 MB / 5 s) and persists the copyCursor only after the flush resolves; maybeFinishCopy forces a final flush before removing the cursor.
    • seqUpdateEndTxn wraps every plain sequence-update end_txn (SEQUENCE_ID_UPDATE, REMOTE_SEQUENCE_UPDATE, blob-drain re-emit); its onCommit flushes the snapshot rows before [seq]=copyStartTime is persisted. core awaits onCommit then calls updateRecordedSequenceId, so this orders flush-before-seq; a flush rejection skips the seq persist so the copy re-runs rather than losing rows. The per-batch endTxnEvent.onCommit carries the same gate (defense-in-depth).
    • Crash before a flush → re-copy idempotently from the last durable cursor (put-if-newer). Dual-engine flush (RocksDB flush() / LMDB flushed).

Relationship to #483

Complementary and non-overlapping (#483 paces the send-side socket flush; this is the receive-side apply). Shares the 5 s cadence + replication_copy* env convention; can stack/rebase once #483 lands. (Did not reuse #483's createCopyFlushPacer to keep this PR independently mergeable; worth consolidating after #483 merges.)

Review notes

Cross-model reviewed (Codex, 4 rounds): CRDT redelivery double-apply → C2 gate; seq cursor not gated on flush → seqUpdateEndTxn at all emit sites; LMDB flush()/replay-cursor divergence → RocksDB-gating + dual-engine flush; async COPY_COMPLETE interleaving → latched status. No open findings; a Gemini pass is running and I'll fold anything it surfaces. Coverage analysis: C2 + the interim retention-guard (separate) cover the mass O(n)-spin paths, so the txn-log index work is optional (demoted).

Generated by an LLM (Claude Opus 4.8).

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@claude

claude Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found. The new commit (fcc403d) is a mechanical core submodule pointer bump. Findings 1 and 2 from the prior round (copyFlushRetryTimer cleanup on WS close; copyFlushInFlight not reset on COPY_START) remain in the code — re-evaluated as non-blocking on re-analysis, but worth a follow-up cleanup.

kriszyp added a commit that referenced this pull request Jun 25, 2026
@kriszyp
kriszyp marked this pull request as ready for review June 25, 2026 10:33
@kriszyp
kriszyp requested a review from a team as a code owner June 25, 2026 10:33
@kriszyp kriszyp changed the title Apply bulk base-copy as durable snapshots, not transactions (#480) copyApply: snapshot non-system base-copy rows without audit entries (part of #480) Jun 25, 2026
@kriszyp

kriszyp commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

Scope clarification (post-CI iteration).

copyApply applies to non-system databases only. The system DB drives cluster machinery off the audit aftercommit stream — hdb_nodes → subscribeToNodeUpdates (peer discovery / connection setup) and hdb_certificate → CA install — which copyApply's no-audit writes suppress, so a freshly-copied node couldn't form its cluster (the integration-test failures on shards 1/2/4 were exactly this). It's now gated via copyApplyActive() = non-system RocksDB; the system copy reverts exactly to main's behavior.

Consequently this PR does not fix #480's system-analytics copy spin (hdb_analytics lives in the system DB) — that is handled separately by the retention-horizon dedup guard, which removes the O(n) keyed-lookup cost without dropping audit entries. This PR is the non-system base-copy optimization (perf + no txn-log amplification on data copies).

Also added: backoff (not busy-loop) on a persistent copy-cursor flush failure.

Possible follow-up (discussion): a txn-log "table-reload" marker would let copyApply safely cover the system DB too — subscribers that need back-filled data react to the marker by reloading from the table, instead of relying on per-row events.

— generated by an LLM (Claude Opus 4.8)

kriszyp and others added 5 commits June 25, 2026 05:52
…sactions (#480)

Receive-side half of copyApply (paired with harper core). Marks base-copy frames
older than copyStartTime as snapshot applies (event.isCopyApply) so core writes them
without an audit/transaction-log entry — removing the O(n) keyed-dedup spin and the
transaction-log amplification when copying an analytics-laden table (#480).

C2: only rows with version < copyStartTime are snapshotted; rows >= copyStartTime are
re-delivered by the post-copy audit replay and keep real audit entries so the
redelivery dedups (no commutative double-apply). The copy-frame status is latched once
per WS message body (the decode loop awaits and ws does not serialize async handlers,
so COPY_COMPLETE could otherwise flip mid-body and drop trailing rows to the audited
path).

Durability: copied DBs run WAL-off and these rows carry no transaction-log entry, so
the resume cursor and the [seq] watermark must only advance behind an explicit RocksDB
flush (memtable -> SST). flushDurableCopyCursor flushes on a 64MB/5s cadence and
persists the cursor only after the flush resolves; seqUpdateEndTxn gates every plain
sequence-update end_txn (and the per-batch end_txn onCommit) so [seq]=copyStartTime is
persisted only after the snapshot rows are durable (core awaits onCommit, then
updateRecordedSequenceId). Dual-engine flush (RocksDB flush() / LMDB flushed).

Bumps core to include the copyApply write mode (harper#1479).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
)

CI integration regression: cluster/copy tests (incl. "Cluster Replication with
LMDB") timed out with cert-trust/1006 churn. The durability flush gate ran for ALL
copies, but only RocksDB copy-apply rows are WAL-off with no transaction-log entry —
LMDB copy rows stay audited (durable via the txn-log) and never needed it. Forcing
the gate on LMDB (and adding an awaited onCommit to EVERY sequence-update end_txn)
destabilized copy/cluster formation.

Scope the gate to STORAGE_IS_ROCKSDB:
- seqUpdateEndTxn returns a plain end_txn for normal replication, LMDB, and mid-copy
  updates below copyStartTime (exactly as before — no per-seq-update overhead); only
  the RocksDB copy-final update (localTime >= copyStartTime) carries the flush onCommit.
- flushDurableCopyCursor persists the cursor directly for LMDB (original behavior);
  the RocksDB flush gate is unchanged.
- The per-batch endTxnEvent onCommit flush gate is likewise RocksDB-gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The system DB drives event-based machinery off the audit `aftercommit` stream:
hdb_nodes feeds subscribeToNodeUpdates (peer discovery / connection setup) and
hdb_certificate feeds CA install. copy-apply suppresses audit entries, so copying the
system DB left a fresh node unable to discover peers or trust certs — cluster never
connected (cert-CA errors, 1006 churn, "connect nodes" timeouts across integration
shards 1/2/4).

Scope copy-apply AND its WAL-off durability gate to non-system RocksDB copies via
copyApplyActive(): the system copy reverts exactly to main's behavior (audited rows,
events fire, direct cursor persist, plain seq-updates), while data-DB copies keep
copy-apply. The system-analytics spin (#480) is handled by the retention-horizon
dedup guard instead, which fixes the O(n) lookup without removing audit entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a persistent copy-cursor flush failure (disk-full / I/O error), the prior .catch
re-staged the cursor and the .finally re-invoked maybeFinishCopy → flushDurableCopyCursor
immediately, busy-looping at ~100% CPU and flooding logs. Add an escalating backoff
(250ms → 30s cap) gating the flush, set on failure with a scheduled retry, and reset on
success. A transient error self-heals; a persistent one idles until the operator (or a
higher-level watchdog) acts, without dropping the connection. (Gemini cross-model review.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ain)

Points the core submodule at core/main 28db4fde4 (harper#1479 merged),
the rebased-onto-main equivalent of the RC's core copyApply commit.

Co-Authored-By: Claude Sonnet 4.6 <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