Skip to content

Abort-mark failed transaction-log batches by default, so logged retries are bounded and retried duplicates are deduplicated - #3928

Merged
kriszyp merged 17 commits into
mainfrom
feat/abort-markers-by-default
Nov 3, 2026
Merged

kriszyp merged 17 commits into
mainfrom
feat/abort-markers-by-default

Conversation

@kriszyp

@kriszyp kriszyp commented Nov 2, 2026 •

Copy link
Copy Markdown
Member
Kyle stamps VOID on a failed delivery crate so the dogs skip it, while old unstamped ledger pages stay roped off.

⊙ Problem

When a transaction writes its log batch and then fails, readers still treat that entry as committed. So Harper never gives up on a logged retry: under contention it retries without limit. A retried replicated write also skips duplicate checks, so it can apply an already-committed duplicate twice (#3453, #3513).

🤔 Is this the right requirement?

Yes. It is the 5.4 plan from #3253. Setting it to false keeps today's behavior.

💡 Solution

  • storage.rocks.transactionLogAbortMarkers is now on by default. rocksdb-js marks a failed batch aborted, and every reader skips it.
  • A logged commit can now fail with a 500 after 41 attempts that conflicted at commit (noteRetryBoundary), or a 503 at the deadline. A chain link after a committed store still retries until it commits (mustRetryLoggedCommit; ⚠️ look hardest here).
  • New storage.rocks.transactionLogWriteIntents (default off, latchWriteIntents) refuses a commit before its log write while an older transaction holds a write intent. The retry then re-logs.
  • A retried replicated write runs the normal duplicate checks, including past the 1000-entry walk cap (mayReadOwnOrphan; ⚠️ and here).

🤔 After a restart, retry dedup on a log stays off until its older files are purged

The owner accepted this window of about 3 days, during which a twin delivery can double an increment or CRDT add. That matches main but regresses v5.3.5 (#3524). rocksdb-js #980 and the walk cap bound the scan cost; #3941 would make certification survive restarts.

⚖️ Alternatives

Six alternatives rejected

🤔 Lease expiry and commit suspension still wait on a logged retry

Refusing one write in a write-once batch would still publish that write's entry. Giving up the whole handle is tracked in #3905.

🔧 Changes

  • resources/DatabaseTransaction.ts: noteRetryBoundary counts conflicts found at commit and re-logs refused attempts. mustRetryLoggedCommit decides the cap and the deadline. abortedWithoutLog lets rollback and blob cleanup run only when the abort left nothing a reader can apply: no batch, or a batch that was marked. An abort that cannot be marked logs an error, and a deadline 503 then reports retryable: false.
  • resources/Table.ts: mayReadOwnOrphan now gates the five audit-log duplicate checks. readsOwnStaging is unchanged.
  • resources/auditStore.ts: establishAbortMarkedHistory and auditHistoryAbortMarked record, per log, which files predate this process.
  • resources/transactionLogAbortMarkers.ts (new; both settings), utility/hdbTerms.ts, databases.ts, residencyPolicy.ts: the default, set once per process and passed to the workers the main thread starts afterwards.
  • resources/originClosedFloor.ts: rekeyLocalRetry. RocksTransactionLogStore.ts: the re-log gate. RecordEncoder.ts and stagesLog: a re-log staging counts as a fresh one, so a write claims its blob again only once its entry is staged.
  • .github/workflows/review-coverage.yml: lists the new module among the storage-binding framing paths, as the policy test requires.
  • resources/DESIGN.md, resources/MAP.md: notes for both modes.

✅ Verification

  • New abortMarkersAdoption.test.js and the ported Stop retried replicated writes from re-applying a duplicate that already committed #3453 set in replicatedRetryDuplicate.test.js (run with markers on and off) cover real conflicts, a real write-intent refusal, the 500, the 503, chain links, the derived-index runtime, the walk cap and pre-marker orphans. The tests of the new behavior fail on the markers-off (main) path.
  • All three test:unit:resources shards are green, and 81 of 81 integration tests in a related subset pass. The hot-key commit bench shows markers on vs off within noise.
  • PR QA: Do concurrent conflicting writes across workers keep every acknowledged write exactly once? — not finished in 90m @ c8dfa03 (queued behind the fleet's QA lane), qa-pr-harper-3928-c8dfa03
  • Release note: a 5.3 build older than the rocksdb-js 2.13.0 pin must not read logs a 5.4 node wrote.
  • Docs: HarperFast/documentation#780 covers the default change, the downgrade requirement and the write-intents setting. Release-note tracking: #3942.
Details
  • Fails on base: with markers off (main's code paths), the cap, 503 and re-log tests hang or fail. The past-cap duplicate is applied twice, and Stop retried replicated writes from re-applying a duplicate that already committed #3453's six residual cases fail. Forcing the history to certified drops the write in the pre-marker orphan case.
  • Pinned off: suites that fake conflicts without RocksDB (noAbortAfterTxnLog, requestPathRetryExhaustion, one test in commitConflictDeadline, and the forced-conflict helper in immediateTransactionConflictRetry) now pin markers off, where their assertions hold. shardingSupport now sets the setting off explicitly where it expects a refusal.
  • test:unit:main: 1242 passing and 0 failing when a 50-minute local cap stopped it; CI runs the full gate. The only resources-shard failures were full-text and subscription timing tests, which also fail with markers off or pass on a rerun.
  • Bench, 2 runs each (8 threads, commits/s, off vs on): hot4 2134 and 2247 vs 2089 and 2373; cold1 9468 and 7530 vs 8901 and 7763; cold4 6723 and 6215 vs 6484 and 6579. 0 errors, and every count check passed.

Supersedes #3453. Closes #3513: with markers on, the default, a retry past the walk cap now deduplicates once its log's pre-process files are purged. Refs #3253.

🤖 Generated by Claude Opus 5.5 with Claude Code; posted via @kriszyp.

Release target: main — the 5.4 default change; v5.3 already reads markers (#3828) and keeps them opt-in
Related PRs: #3453 supersedes, #3524 overlaps (the v5.3 port of #3453; main gets this PR instead), #3728 overlaps (also changes retry staging in resources/RecordEncoder.ts), #3910 overlaps (open; also edits resources/Table.ts and resources/DESIGN.md), #3856 overlaps (merged; the DESIGN.md cut this branch rebased onto), 13 others independent
Complexity: complicated

Deferred findings

  • 357dfee6eed04dd2 minor reliability — resources/DESIGN.md — logged retries still bypass suspension/lease refusal with markers on, prolonging drain — filed With abort markers on, let suspension and lease expiry give up a logged retry #3905
  • 2e694142be678601 minor data-integrity — resources/Table.ts — audit-only writeCommit(false) appends a blob-bearing audit entry without setting blobsAuditReferenced; a later skip/supersede deletes the bl — filed An audit-only staging can lose a blob its committed audit entry references #3912
  • 8e3b71330068c463 nit data-integrity — resources/transactionLogAbortMarkers.ts — default-on markers write aborted batches that older log readers treat as live — NOT FILED
  • 968b54995b7248e9 nit documentation — resources/transactionLogAbortMarkers.ts — default-on, restart-only abort-marker (and opt-in write-intent) setting lacks operator documentation — NOT FILED

Late Grok findings

4 surviving (highest major) — Grok reviewed 77121e9
  • major — resources/databases.ts:502 — abort markers now default on for every audited RocksDB commit and every transaction-log reader, and the PR includes no throughput or latency measurement
  • minor — resources/transactionLogAbortMarkers.ts:17 — a worker created before the main thread latches resolves the setting itself. When that worker opens a database, it can write a different value into the process-global native setting
  • nit — resources/residencyPolicy.ts:39 — the attribute-sharding gate reads the live config, not the latched value
  • nit — static/defaultConfig.yaml:87 — operators are not told about this setting: storage.rocks has no transactionLogAbortMarkers entry and no operator doc mentions it

Not standing: none. Grok ran 19m (19m from launch to report); the fold adjudication (cursor-grok@77121e91fdca-dbd456fb, claude) took 1m.

Late Grok findings

8 surviving (highest major) — Grok reviewed f3e5cae
  • major — resources/transactionLogAbortMarkers.ts:17 — a worker that loads before main publishes the latch picks its own policy, and resources/databases.ts:502 writes that choice into process-global native config
  • minor — resources/DatabaseTransaction.ts:3040 — abandonScope clears rotatedAfterCommit (:3798) while a chain commit is in flight, so the pending link can be abandoned after an earlier segment committed, and the deadline error reports retryable
  • minor — resources/DatabaseTransaction.ts:405 — at the retry cap, ERR_TRANSACTION_ABORT_UNRESOLVED is swallowed and both cap exits (:3376-3378, :3595-3597) return the generic 500 while crash replay can still apply the batch
  • minor — resources/DatabaseTransaction.ts:1622 — a re-logged retry that ends up skipped keeps blobsAuditReferenced/appendedTo; the same-handle exemption at :3490 leaks the blob file
  • minor — resources/residencyPolicy.ts:39 — attribute-sharding admission reads live config while native storage follows the latched value
  • nit — resources/originClosedFloor.ts:420 — rekeyLocalRetry declines caller-value reservations, so the retry re-appends at its original minted key
  • nit — resources/Table.ts:6205 — added comments repeat the orphan rationale and narrate code and test steps
  • nit — resources/transactionLogAbortMarkers.ts:13 — nothing tells operators about the new default, the false opt-out, or the whole-process restart requirement

Not standing: 7 merged. Grok ran 10m (11m from launch to report); the fold adjudication (cursor-grok@f3e5cae0b9fd-6c0a7878, claude) took 1m.

Late Grok findings

4 surviving (highest nit) — Grok reviewed 1564a87
  • nit — resources/residencyPolicy.ts:39 — the attribute-sharding gate reads the live config value instead of the latched setting
  • nit — resources/DatabaseTransaction.ts:4050 — the "Abandoning a write transaction" error log is built before the abort runs, so it says neither "not retryable" nor "outcome unknown" when the abort then fails to mark the batch
  • nit — unitTests/resources/abortMarkersAdoption.test.js:317 — the blob regression test releases the holder on a delay(100) timer instead of after an observed pre-log refusal
  • nit — resources/transactionLogAbortMarkers.ts:13 — operators have no guidance yet: the setting is now on by default, false opts out, a change needs a whole-process restart, and a batch this process aborts is read as committed by an older reader

Not standing: 1 merged. Grok ran 16m (16m from launch to report); the fold adjudication (cursor-grok@1564a874b25d-060511aa, claude) took 1m.

Ready-For-Promotion: 5fb516d urgency=normal late=folded

Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; declined=muse; rounds=12; full=4 @ 5fb516d

Review-Attention: deep ~50m (critical: DatabaseTransaction.ts, RecordEncoder.ts +5; sensitive: review-coverage.yml; decisions: write-intents-opt-in, dedup-certification-window, markers-default-on, latch-local-fallback, do-less-alternative, release-target-unclear) @ 5fb516d

kriszyp and others added 10 commits November 2, 2026 12:44
storage.rocks.transactionLogAbortMarkers is now on unless configured false. With markers on, a
logged retry is capped by conflicts RocksDB found at commit and bounded by the commit deadline, a
retry refused before its log write re-logs under a fresh key, and retried replicated duplicates are
deduplicated (including past the audit walk's depth cap) once the audit store's retained history is
all marker-era. Markers off keeps today's behavior.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mit past its deadline

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ommitted-predecessor test

The durable certification record could outlive a downgrade to a build that writes unmarked
batches, so each audit-store open now records, per log, the file the process started in, and
retry dedup trusts a log once those files are purged. A lock() root (snapshotFree, nothing
committed) now gets the cap and deadline; only a link after a committed store keeps retrying.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w findings on the abort-marker paths

A link refused before its log write after an earlier store committed now keeps retrying like a
logged one; a lock() root reports a retryable 503; a marked cap or deadline abort counts as a
discarded write; a terminal error after a re-logged attempt clears relogOnRetry; and a re-log
drops the blob references of the batch that was never written.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resetting blobsAuditReferenced on a re-log could let an audit-only re-staging lose a blob a kept
batch still references; a leaked file is the safe direction.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e doc cut

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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 implements support for transaction-log abort markers in RocksDB, introducing process-global latching of the setting, tracking abort markers on aborted transactions, and adjusting retry boundaries, conflict budgets, and deduplication logic accordingly. The review identified two critical issues in resources/DatabaseTransaction.ts: a ReferenceError caused by using an undefined txn variable in abortNativeTransaction, and a potential TypeError in abortedWithoutLog when transaction is nullish but error?.hasLog is true.

Comment thread resources/DatabaseTransaction.ts Resolved
Comment thread resources/DatabaseTransaction.ts Resolved
kriszyp and others added 2 commits November 2, 2026 13:51
…paths

It imports a storage binding, which the framing-policy test requires to be listed.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scope can end while its later segment is still committing, and that segment must keep retrying.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bort could not be marked

A re-log discards the attempt's batch, so its writes no longer claim their blobs until the retry
stages an entry again (a re-log staging now counts as a fresh one). At the retry cap, an abort
rocksdb-js could not mark leaves the batch for crash replay, and the 500 now says so.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kriszyp and others added 2 commits November 2, 2026 14:29
… marked; assert the refusal in the blob test

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the restart dedup window

The setting is latched and passed to RocksDatabase.config() like transactionLogAbortMarkers, so the
re-log path is reachable from configuration. resources/DESIGN.md now states that after a restart a
log's retry dedup waits for its older files to purge.

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kriszyp and others added 2 commits November 2, 2026 15:16
…tests; cover the unmarked deadline 503

Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-abort-marker-adoption
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kriszyp
kriszyp marked this pull request as ready for review November 3, 2026 03:30
@kriszyp
kriszyp merged commit 54e1f0f into main Nov 3, 2026
76 checks passed
@kriszyp
kriszyp deleted the feat/abort-markers-by-default branch November 3, 2026 03:30
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.

A retried replicated write can re-apply a committed duplicate buried beyond the audit walk's depth cap

1 participant