Repository navigation
Abort-mark failed transaction-log batches by default, so logged retries are bounded and retried duplicates are deduplicated - #3928
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
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.
…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>
… 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>
…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>
This was referenced Nov 3, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ 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
falsekeeps today's behavior.💡 Solution
storage.rocks.transactionLogAbortMarkersis now on by default. rocksdb-js marks a failed batch aborted, and every reader skips it.noteRetryBoundary), or a 503 at the deadline. A chain link after a committed store still retries until it commits (mustRetryLoggedCommit;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.mayReadOwnOrphan;🤔 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 matchesmainbut 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
main.🤔 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:noteRetryBoundarycounts conflicts found at commit and re-logs refused attempts.mustRetryLoggedCommitdecides the cap and the deadline.abortedWithoutLoglets 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 reportsretryable: false.resources/Table.ts:mayReadOwnOrphannow gates the five audit-log duplicate checks.readsOwnStagingis unchanged.resources/auditStore.ts:establishAbortMarkedHistoryandauditHistoryAbortMarkedrecord, 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.tsandstagesLog: 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
abortMarkersAdoption.test.jsand the ported Stop retried replicated writes from re-applying a duplicate that already committed #3453 set inreplicatedRetryDuplicate.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.test:unit:resourcesshards 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.Details
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.noAbortAfterTxnLog,requestPathRetryExhaustion, one test incommitConflictDeadline, and the forced-conflict helper inimmediateTransactionConflictRetry) now pin markers off, where their assertions hold.shardingSupportnow 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.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
357dfee6eed04dd2minor 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 #39052e694142be678601minor 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 #39128e3b71330068c463nit data-integrity —resources/transactionLogAbortMarkers.ts— default-on markers write aborted batches that older log readers treat as live — NOT FILED968b54995b7248e9nit documentation —resources/transactionLogAbortMarkers.ts— default-on, restart-only abort-marker (and opt-in write-intent) setting lacks operator documentation — NOT FILEDLate Grok findings
4 surviving (highest major) — Grok reviewed 77121e9
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 measurementresources/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 settingresources/residencyPolicy.ts:39— the attribute-sharding gate reads the live config, not the latched valuestatic/defaultConfig.yaml:87— operators are not told about this setting:storage.rockshas notransactionLogAbortMarkersentry and no operator doc mentions itNot 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
resources/transactionLogAbortMarkers.ts:17— a worker that loads before main publishes the latch picks its own policy, andresources/databases.ts:502writes that choice into process-global native configresources/DatabaseTransaction.ts:3040—abandonScopeclearsrotatedAfterCommit(: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 retryableresources/DatabaseTransaction.ts:405— at the retry cap,ERR_TRANSACTION_ABORT_UNRESOLVEDis swallowed and both cap exits (:3376-3378,:3595-3597) return the generic 500 while crash replay can still apply the batchresources/DatabaseTransaction.ts:1622— a re-logged retry that ends up skipped keepsblobsAuditReferenced/appendedTo; the same-handle exemption at:3490leaks the blob fileresources/residencyPolicy.ts:39— attribute-sharding admission reads live config while native storage follows the latched valueresources/originClosedFloor.ts:420—rekeyLocalRetrydeclines caller-value reservations, so the retry re-appends at its original minted keyresources/Table.ts:6205— added comments repeat the orphan rationale and narrate code and test stepsresources/transactionLogAbortMarkers.ts:13— nothing tells operators about the new default, thefalseopt-out, or the whole-process restart requirementNot 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
resources/residencyPolicy.ts:39— the attribute-sharding gate reads the live config value instead of the latched settingresources/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 batchunitTests/resources/abortMarkersAdoption.test.js:317— the blob regression test releases the holder on adelay(100)timer instead of after an observed pre-log refusalresources/transactionLogAbortMarkers.ts:13— operators have no guidance yet: the setting is now on by default,falseopts out, a change needs a whole-process restart, and a batch this process aborts is read as committed by an older readerNot 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