Repository navigation
Bound a request-path commit's conflict retries to its queue-time budget - #2459
Merged
Merged
Conversation
rocksdb-js 2.8 returns control from a commit parked on a conflicting write intent every ROCKSDB_JS_PARK_TIMEOUT_MS even when the holder never releases, so Harper sees a stream of transient conflicts rather than one hung commit. The 40-attempt cap alone then keeps the initiating request pending for roughly 40 park timeouts - minutes past the queue limit an operator configured, which is what left a control-plane node rejecting every write for 12 minutes. Carry one elapsed clock per logical commit on its chain root, stamped at the first native submission, and read it at both retry decisions. Past max(storage.maxTransactionQueueTime, timeoutBudget) the commit takes the existing retry-exhaustion abort path, logs the stuck-commit identity through the same rate limiter checkOverloaded() uses, and throws a 503 TRANSACTION_COMMIT_CONFLICT_TIMEOUT. Only a chain root that has not rotated through a mid-scope commit reports retryable: anywhere else an earlier store already landed durable audit entries a replayed request would repeat. Outstanding-commit tracking stays per native attempt - it measures native liveness and drives thread-wide load shedding, so back-dating it would let one uncapped source-apply retry shed every unrelated request on the thread. Source-applied writes stay exempt from the deadline as they are from the cap. Refs #2450 Co-Authored-By: Claude Opus <noreply@anthropic.com>
- Give the abandonment log its own cooldown slot. It shared checkOverloaded()'s, and that one fires on every bystander write during a wedge, so the line naming the abandoned transaction - the only server-side record of why a request was failed - lost the slot to a line naming a different commit. - Add a test that observes production arming the clock at the first native submission and releasing it at settle. Every other case plants the clock, so the one line that arms it could have been deleted with the suite still green. - Pin the snapshotFree half of the retryable decision: a scope that landed an earlier mid-handler segment must report retryable: false. - Trim two comments that restated the adjacent code. Co-Authored-By: Claude Opus <noreply@anthropic.com>
Co-Authored-By: Claude Opus <noreply@anthropic.com>
Pre-existing drift from #2437 under the current prettier 3.9.x; formatting only, no behavior change. Format Check fails identically on main without it. Co-Authored-By: Claude Opus <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to track and enforce deadlines for transaction commit conflict retries, preventing requests from hanging indefinitely on locked write intents. It adds the TransactionCommitConflictTimeoutError (503) and implements budget checks on logical commits. Review feedback recommends aligning null-or-undefined checks with the repository style guide by using loose equality (== null), using strict assertions (assert.strictEqual and assert.deepStrictEqual) in tests to avoid type-coercion bugs, and replacing condition-polling helpers with deterministic watchdog timers in tests.
Co-Authored-By: Claude Opus <noreply@anthropic.com>
kriszyp
marked this pull request as ready for review
September 2, 2026 17:15
github-actions
Bot
requested review from
Ethan-Arrowood,
cb1kenobi and
heskew
September 2, 2026 17:16
kriszyp
added a commit
that referenced
this pull request
Sep 3, 2026
…inks Rebased onto main (#2459 landed the `describeCommitIdentity`/`allowStuckCommitLog` refactor under the same log line), then applied the open review feedback: - Holder candidates now enumerate every registry entry, ranking the stuck commit's own database first and labelling a foreign one with its path. The verification table is one process-global slot array whose hash mixes in the database id, so a holder in `system` or `oauth` parks a `data` commit at the same rate another `data` key would; filtering to the commit's own database printed nothing at all for that shape. - The enumeration guards `database.path` before `resolve()`. Unguarded, one pathless entry anywhere in the registry threw inside the predicate and zeroed the candidate list for every database, silently. - `describeHolderCandidates()` honors the documented disable value, like the other two surfaces. - The same suffix is appended to `abandonCommitAfterDeadline()`'s log, the other place a commit parked on someone else's write intent is reported. - Changing `storage.longTransactionReportThreshold` clears the accrued backoff, so a handle already under observation is re-measured. `nextReportAgeMs` was pinned at first observation, so lowering the threshold mid-incident did not bring a report forward and raising it did not quiet one — contradicting the live-reload the module promises. - Attribution walks the whole `.next` chain, reporting each link that holds its own native handle under that link's own id, with that link's own staged-write count. A link the monitor reaches only through the root was named under the root's id, which the registry sweep's line cannot be joined to. - The attribution prune runs at most once a minute rather than once per over-threshold transaction per tick (O(N^2) in exactly the leak it reports on). - A handle whose database has no path logs `?` rather than `undefined`; strict assertion in the new txn-tracking test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 3, 2026
…inks Rebased onto main (#2459 landed the `describeCommitIdentity`/`allowStuckCommitLog` refactor under the same log line), then applied the open review feedback: - Holder candidates now enumerate every registry entry, ranking the stuck commit's own database first and labelling a foreign one with its path. The verification table is one process-global slot array whose hash mixes in the database id, so a holder in `system` or `oauth` parks a `data` commit at the same rate another `data` key would; filtering to the commit's own database printed nothing at all for that shape. - The enumeration guards `database.path` before `resolve()`. Unguarded, one pathless entry anywhere in the registry threw inside the predicate and zeroed the candidate list for every database, silently. - `describeHolderCandidates()` honors the documented disable value, like the other two surfaces. - The same suffix is appended to `abandonCommitAfterDeadline()`'s log, the other place a commit parked on someone else's write intent is reported. - Changing `storage.longTransactionReportThreshold` clears the accrued backoff, so a handle already under observation is re-measured. `nextReportAgeMs` was pinned at first observation, so lowering the threshold mid-incident did not bring a report forward and raising it did not quiet one — contradicting the live-reload the module promises. - Attribution walks the whole `.next` chain, reporting each link that holds its own native handle under that link's own id, with that link's own staged-write count. A link the monitor reaches only through the root was named under the root's id, which the registry sweep's line cannot be joined to. - The attribution prune runs at most once a minute rather than once per over-threshold transaction per tick (O(N^2) in exactly the leak it reports on). - A handle whose database has no path logs `?` rather than `undefined`; strict assertion in the new txn-tracking test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H
kriszyp
added a commit
that referenced
this pull request
Sep 4, 2026
…inks Rebased onto main (#2459 landed the `describeCommitIdentity`/`allowStuckCommitLog` refactor under the same log line), then applied the open review feedback: - Holder candidates now enumerate every registry entry, ranking the stuck commit's own database first and labelling a foreign one with its path. The verification table is one process-global slot array whose hash mixes in the database id, so a holder in `system` or `oauth` parks a `data` commit at the same rate another `data` key would; filtering to the commit's own database printed nothing at all for that shape. - The enumeration guards `database.path` before `resolve()`. Unguarded, one pathless entry anywhere in the registry threw inside the predicate and zeroed the candidate list for every database, silently. - `describeHolderCandidates()` honors the documented disable value, like the other two surfaces. - The same suffix is appended to `abandonCommitAfterDeadline()`'s log, the other place a commit parked on someone else's write intent is reported. - Changing `storage.longTransactionReportThreshold` clears the accrued backoff, so a handle already under observation is re-measured. `nextReportAgeMs` was pinned at first observation, so lowering the threshold mid-incident did not bring a report forward and raising it did not quiet one — contradicting the live-reload the module promises. - Attribution walks the whole `.next` chain, reporting each link that holds its own native handle under that link's own id, with that link's own staged-write count. A link the monitor reaches only through the root was named under the root's id, which the registry sweep's line cannot be joined to. - The attribution prune runs at most once a minute rather than once per over-threshold transaction per tick (O(N^2) in exactly the leak it reports on). - A handle whose database has no path logs `?` rather than `undefined`; strict assertion in the new txn-tracking test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 4, 2026
…inks Rebased onto main (#2459 landed the `describeCommitIdentity`/`allowStuckCommitLog` refactor under the same log line), then applied the open review feedback: - Holder candidates now enumerate every registry entry, ranking the stuck commit's own database first and labelling a foreign one with its path. The verification table is one process-global slot array whose hash mixes in the database id, so a holder in `system` or `oauth` parks a `data` commit at the same rate another `data` key would; filtering to the commit's own database printed nothing at all for that shape. - The enumeration guards `database.path` before `resolve()`. Unguarded, one pathless entry anywhere in the registry threw inside the predicate and zeroed the candidate list for every database, silently. - `describeHolderCandidates()` honors the documented disable value, like the other two surfaces. - The same suffix is appended to `abandonCommitAfterDeadline()`'s log, the other place a commit parked on someone else's write intent is reported. - Changing `storage.longTransactionReportThreshold` clears the accrued backoff, so a handle already under observation is re-measured. `nextReportAgeMs` was pinned at first observation, so lowering the threshold mid-incident did not bring a report forward and raising it did not quiet one — contradicting the live-reload the module promises. - Attribution walks the whole `.next` chain, reporting each link that holds its own native handle under that link's own id, with that link's own staged-write count. A link the monitor reaches only through the root was named under the root's id, which the registry sweep's line cannot be joined to. - The attribution prune runs at most once a minute rather than once per over-threshold transaction per tick (O(N^2) in exactly the leak it reports on). - A handle whose database has no path logs `?` rather than `undefined`; strict assertion in the new txn-tracking test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H
kriszyp
added a commit
that referenced
this pull request
Sep 5, 2026
…inks Rebased onto main (#2459 landed the `describeCommitIdentity`/`allowStuckCommitLog` refactor under the same log line), then applied the open review feedback: - Holder candidates now enumerate every registry entry, ranking the stuck commit's own database first and labelling a foreign one with its path. The verification table is one process-global slot array whose hash mixes in the database id, so a holder in `system` or `oauth` parks a `data` commit at the same rate another `data` key would; filtering to the commit's own database printed nothing at all for that shape. - The enumeration guards `database.path` before `resolve()`. Unguarded, one pathless entry anywhere in the registry threw inside the predicate and zeroed the candidate list for every database, silently. - `describeHolderCandidates()` honors the documented disable value, like the other two surfaces. - The same suffix is appended to `abandonCommitAfterDeadline()`'s log, the other place a commit parked on someone else's write intent is reported. - Changing `storage.longTransactionReportThreshold` clears the accrued backoff, so a handle already under observation is re-measured. `nextReportAgeMs` was pinned at first observation, so lowering the threshold mid-incident did not bring a report forward and raising it did not quiet one — contradicting the live-reload the module promises. - Attribution walks the whole `.next` chain, reporting each link that holds its own native handle under that link's own id, with that link's own staged-write count. A link the monitor reaches only through the root was named under the root's id, which the registry sweep's line cannot be joined to. - The attribution prune runs at most once a minute rather than once per over-threshold transaction per tick (O(N^2) in exactly the leak it reports on). - A handle whose database has no path logs `?` rather than `undefined`; strict assertion in the new txn-tracking test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H
kriszyp
added a commit
that referenced
this pull request
Sep 8, 2026
* Report the long-lived transaction holder behind a wedged commit A RocksDB transaction that holds staged writes — and with them verification-table write intents — can stay open indefinitely with nothing in core naming it. On 5.2.7 such a holder parked other writers forever; on 5.2.8 it costs ~5s per colliding write and can block version reclamation until restart. Two field incidents held for 4.5h and 9h with no log line identifying the holder. Nothing reported it because the monitor's `sourceApply`/`isReplay` branch force-commits silently, its commit-phase sparing carries no native transaction id, `checkOverloaded()` names only the victim commit, and a handle opened outside a supervised transaction is in neither monitor registry at all. Three reporting surfaces, joined by the native transaction id: - `resources/longLivedTransactions.ts` sweeps rocksdb-js's registry from the main thread — it is process-global, so one sweep sees every worker's handles, including ones core never tracked — and names any open past `storage.longTransactionReportThreshold` (default 5m, 0 disables). - The per-thread monitor adds the attribution the sweep cannot: thread, database/table, `startedFrom`, staged write count, and which state kept it from being reaped. - `checkOverloaded()` appends the live handles on the stuck commit's own database as holder candidates. Reporting only: no reap predicate, timeout transition, commit or abort order changes. Suppression is keyed on (database path, native id) because ids are per-descriptor and restart on reopen, and candidates are ranked by age but never filtered by it — a coordinated retry can park on a transaction younger than itself. Refs #2471 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016a9iHeo3Zz4vjtak9mFpoj * Address pre-push review findings - Count staged writes only when a line is actually due. The count walks every chain link's whole `writes` array, and the backoff that suppresses the log lives downstream, so a replay or canonical-source apply holding ~10^5 writes for hours paid that walk on every 30s tick to produce a number it logs about five times. It is a thunk now, evaluated after the due check. - Reject a threshold that is neither string nor number. `convertToMS` returns 0 for a boolean or object — what YAML hands us for `longTransactionReportThreshold: yes` — which is indistinguishable from the documented 0 that disables reporting, so a typo silently switched off all three surfaces with no warning. - Report `?` rather than the table name when a store has no `rootStore.databaseName`, matching the stuck-commit line the attribution line is read beside. - Add `setMaxOutstandingTxnDuration`, so a test can reach `checkOverloaded()`'s stuck-commit log without wedging a commit for the real 45s limit, and cover the surface an operator actually reads during an outage: the candidate suffix reaches the message, the wedged commit is excluded from its own candidate list, and a registry that throws still yields the 503 rather than a 500. - Assert a real table store's `rootStore.path` is a path `registryStatus()` reports, which is the join the candidate lookup depends on and the mocked-registry tests cannot catch. - Trim comments that restated the code rather than explaining it. Refs #2471 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016a9iHeo3Zz4vjtak9mFpoj * Address round-2 review nits - Assert the staged-write walk is skipped on a suppressed report, which is the regression guard for the lazy count itself. - Say which degraded case happened: a registry entry with no transactionDetails is not the same as no entry for the path, and only the latter means the store-path join has drifted. - Drop a comment that narrated the call below it. Refs #2471 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016a9iHeo3Zz4vjtak9mFpoj * Address PR review: cross-database candidates, live threshold, chain links Rebased onto main (#2459 landed the `describeCommitIdentity`/`allowStuckCommitLog` refactor under the same log line), then applied the open review feedback: - Holder candidates now enumerate every registry entry, ranking the stuck commit's own database first and labelling a foreign one with its path. The verification table is one process-global slot array whose hash mixes in the database id, so a holder in `system` or `oauth` parks a `data` commit at the same rate another `data` key would; filtering to the commit's own database printed nothing at all for that shape. - The enumeration guards `database.path` before `resolve()`. Unguarded, one pathless entry anywhere in the registry threw inside the predicate and zeroed the candidate list for every database, silently. - `describeHolderCandidates()` honors the documented disable value, like the other two surfaces. - The same suffix is appended to `abandonCommitAfterDeadline()`'s log, the other place a commit parked on someone else's write intent is reported. - Changing `storage.longTransactionReportThreshold` clears the accrued backoff, so a handle already under observation is re-measured. `nextReportAgeMs` was pinned at first observation, so lowering the threshold mid-incident did not bring a report forward and raising it did not quiet one — contradicting the live-reload the module promises. - Attribution walks the whole `.next` chain, reporting each link that holds its own native handle under that link's own id, with that link's own staged-write count. A link the monitor reaches only through the root was named under the root's id, which the registry sweep's line cannot be joined to. - The attribution prune runs at most once a minute rather than once per over-threshold transaction per tick (O(N^2) in exactly the leak it reports on). - A handle whose database has no path logs `?` rather than `undefined`; strict assertion in the new txn-tracking test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H * Address pre-push review: skip sub-threshold state, drop a false `active` - The sweep no longer builds a key, a report state and two container entries for every live handle in the process on every pass. `nextReportAgeMs` is seeded at the threshold and only grows, so a sub-threshold handle can never be due and its state would be pruned on the same pass; a healthy node's minute now costs nothing rather than ~4 allocations per live handle. - A chain link reported through the root no longer claims `state: active`. `timeout` is armed by `addWrite` and decremented once per tick only for the link the tick entered on, so a link reached through `.next` kept an armed `timeout` forever and told the operator the application was still writing when the link had been idle for hours — the inverse of the diagnosis, on exactly the shape the chain walk exists for. - Two comments that restated the callee's own docstring are gone. The chain-link test now matches on the link's table rather than its native id alone: ids are allocated per database descriptor, so the root and the second-database link both held id 4 and an id-only match asserted against the root's line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H * Address re-review: read the clock that is actually decayed for a chain link `active` now reads whichever recency clock the monitor maintains for the link being reported: `timeout` for the link the tick entered on, which is decremented once per tick beside the reap branches, and `writeTimeout` for a link reached only through the chain, which is what `chainStillActive` decays. The previous round dropped `active` for chain links entirely, which traded a permanent false positive for hiding a link the application is still writing to. Both directions are pinned: a freshly-written chain link must report `active`, and one whose write recency has decayed must not. Each is a single-phase test — the reporting window closes when the logical transaction settles, so a multi-phase assertion raced the force-commit — and the child's native id is captured before the wait rather than read after it. Also corrects the sweep comment: without the new skip, a sub-threshold handle's state was added to `seen` and survived pruning. The reason the skip is safe is that `nextReportAgeMs` is seeded at the threshold and only grows, so that state could never change an outcome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H * Correct a docstring the previous commit made stale reportLongLivedLink no longer infers the chain case from `timeout`; it reads whichever recency clock the monitor maintains for that link. The docstring still described the old rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H * Keep both reporting surfaces inside the guards that promise they cannot throw Both functions document that they never take down their caller — the sweep runs from a bare `setInterval` on the main thread, and `describeHolderCandidates` builds a log message on the way to a 503 that must not become a 500. In both, the config read sat outside the `try` that makes that true. Scope moves only; no behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H * Give the cross-database holder a slot the cap cannot take away Ranking the stuck commit's own database first and then capping at three could bury the one candidate the cross-database search was added to surface: a busy database routinely has three live handles of its own, so a `system`/`oauth` holder parking a `data` commit would only ever appear inside the "and N more" count. The oldest foreign candidate now takes the last slot when this database would otherwise fill every one, and a test pins it with four same-database handles against one foreign. Found by the cursor-grok leg, which reached a verdict for the first time this session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H * Read the clock once per monitor tick, not once per link `reportLongLivedLink` sampled `performance.now()` before the threshold check, so a worker paid one clock read per link holding a live handle on every tick — the cost scales with tracked transactions, which is exactly the leak this reports on. The tick now samples once and passes it down; the threshold it feeds is minutes, so per-link precision buys nothing. Also strengthens the reserved-slot test: a second, younger foreign handle now sits between, so the fixture proves the OLDEST foreign candidate takes the slot rather than merely some foreign one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QDdAwWFsWANGNs7ccja1H * Give CI jobs enough time to complete Co-Authored-By: GPT-5 Codex <noreply@openai.com> * Bound long-lived transaction attribution reporting Co-Authored-By: GPT-5 Codex <noreply@openai.com> * Refine long-lived transaction diagnostics Co-Authored-By: GPT-5 Codex <noreply@openai.com> * Reset transaction report backoff on threshold changes Co-Authored-By: GPT-5 Codex <noreply@openai.com> * Restore transaction test seam documentation Co-Authored-By: GPT-5 Codex <noreply@openai.com> * Stabilize long-lived transaction reporting tests Co-Authored-By: GPT-5 Codex <noreply@openai.com> * Harden long-lived transaction diagnostics tests Co-Authored-By: GPT-5 Codex <noreply@openai.com> --------- Co-authored-by: Kris Zyp <kris@harperdb.io> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: GPT-5 Codex <noreply@openai.com>
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.
A request-path RocksDB commit that keeps losing write-intent conflicts now stops at its queue-time budget instead of running the full 40-attempt cap.
rocksdb-js 2.8 wakes a commit parked on another transaction's write intent after
ROCKSDB_JS_PARK_TIMEOUT_MS(5s) and returnsRETRY_NOW_VALUEeven when the holder never releases. That turned the "commit that never settles" of #2450 into a stream of transient conflicts — but the attempt cap alone still keeps the initiating request pending for roughly 40 park timeouts, several times the 45sstorage.maxTransactionQueueTimean operator configured. This adds the missing bound.One elapsed clock per logical commit, stamped on the chain root at its first native submission, is read at both retry decisions (the coordinated
RETRY_NOW_VALUEresolve path and theERR_BUSY/ERR_TRY_AGAINrejection path). PastMath.max(storage.maxTransactionQueueTime, timeoutBudget)the commit takes the existingabortChainAfterRetries()cleanup, logs the stuck-commit identity, and throws a 503TransactionCommitConflictTimeoutErrorcarryingcode: 'TRANSACTION_COMMIT_CONFLICT_TIMEOUT'. The 40-attempt cap stays as the second bound.Three properties are load-bearing:
trackOutstandingCommit()is untouched — it measures native liveness and drivescheckOverloaded()'s thread-wide load shedding, so back-dating it would let one uncappedsourceApplyretry eventually 503 every unrelated request on the thread. The new clock is stamped only when unset, so retry rounds and chained per-store commits inherit it; the budget comparison itself is wheresourceApplyis exempted.retryableis true only when nothing landed durably. A chained link commits solely from its predecessor's success handler, and a head that rotated through a mid-scope commit already landed a segment; in both cases replaying the request would repeat durable audit entries and hooks, so the flag is false — pinned for the chained case and for the mid-scope case.checkOverloaded()'s 1s limiter, but that one fires on every bystander write during a wedge and would have starved the only server-side line naming the abandoned transaction; each site now has its own slot.Source-applied writes are exempt from the deadline exactly as they are from the attempt cap: there is no resubscribe/sequence-resume path, so dropping one permanently diverges the node (harper-pro#348).
For the human reviewer
The step-6 planning gate never returned
chosen-approach-sound. Two rounds returnedFraming-Verdict: better-alternative-exists: the first rejected a ladder-local 45s clock in favour of reusing the outstanding-commit tracker; the second rejected that because back-dating the tracker would make one long source-apply retry shed every unrelated request on the thread. @kriszyp adjudicated and directed the third framing — deadline only on the chain root, tracker per attempt, abandonment-site diagnostic, retryability conditioned on whether the chain half-landed — which is what is implemented here. The design note is not in the diff; its "Approaches considered" set is summarised above.Closesmay overstate what this does. #2450 asks for three things. This delivers the deadline (ask 1) and a distinct error code separating "wedged" from "queue overloaded" (part of ask 3). It does not deliver the holder/key diagnostic (ask 2) — rocksdb-js 2.8 exposes neither the waited-on key nor the intent holder through its publicTransactionAPI — nor cancellation of an already-submitted native commit, nor scopingcheckOverloaded()'s rejection to what actually conflicts (the rest of ask 3). If you want those tracked, downgrade this toRefsand split them out.Declined review findings, all raised and adjudicated pre-push:
save()'s immediate-commit reuse branch and reverted that — I could not reach the branch from a test, becauseResource.tsstarts a fresh transaction for a static op whose context transaction is closed. The residual is bounded: a thread with a permanently wedged commit is already 503ing every application write throughcheckOverloaded(), so the extra spurious 503 is not a new failure mode. Worth a follow-up if you disagree.unhandledRejection. Per commit, not per record, in a path that already builds several promises.Open decisions, unchanged from the design and worth a second opinion:
Math.max(maxTransactionQueueTime, timeoutBudget)means a caller can only ever raise its budget. A request with a 2s client deadline still burns the full configured queue time in retries.retryableis false. Clients that retry on status code alone will replay a half-landed multi-store request; the flag is in the response body (serverHelpers.jssendscode/retryablewheneverretryable !== undefined), not the status.Verification
npm run build,npm run format:write,npm run lint:required— clean. The last commit reformatsunitTests/resources/query-array-scoping.test.js, which is unrelated pre-existing drift from Pin element-scoping semantics of queries over array-valued properties #2437 under the current prettier 3.9.x — Format Check fails on it identically onmain, so it had to be fixed here to get a green run. Formatting only.npm run test:unit:resources— 1929 passing, 0 failing.npm run test:unit:main— 5188 passing, 2 failing; both (gitCredentials,configValidatordomain-socket path length) reproduce unchanged onorigin/mainin this worktree and are environmental.npm run test:integration:all, run in four chunks to fit the runner: 2006 passing, 0 failing. The only failures were theOllamaBackend against a real Ollama instancecases, which need a local Ollama daemon.resources/DatabaseTransaction.tsandutility/errors/hdbError.tsreverted toorigin/mainanddistrebuilt, the three behavioural cases in the new suite fail (they get the generic 500 exhaustion error after 41 attempts instead of the 503).New coverage in
unitTests/resources/commitConflictDeadline.test.js: production arming/release of the clock (so the one line that arms it cannot be deleted with the suite still green), abandonment on both conflict paths, one budget shared across a cross-database chain withretryable: falseand the head's write durable, the mid-scope-rotation half of the retryable decision,sourceApplyretrying to convergence past the deadline, and the clock being released so the next transaction starts fresh. Error shape inunitTests/utility/errors/hdbError.test.js.Closes #2450
Complexity: moderate
🤖 Generated with Claude Code — reviewed by Claude Opus 5
Review-Coverage: authored=claude; ran=codex; blocked=gemini(auth); declined=cursor-grok,cursor-composer,domain; rounds=5 @ d8d49de
Human-Review-Need: 3 @ d8d49de