Skip to content

Commit f469ae9

Browse files
heskewclaude
authored andcommitted
fix(table): don't double-apply re-delivered commutative op in capped out-of-order walk (#1137)
In the RocksDB-only bounded out-of-order audit-chain walk (the #1114 depth cap), the walk stops before reaching txnTime, so the inline duplicate check never runs. The fallback used auditStore.get(txnTime, ...), which queries the transaction log — a just-committed entry from a back-to-back re-delivery is not reliably visible there, so a re-delivered commutative op was double-applied (count 3 -> 6). The failure was timing-sensitive: green in isolation, red under load, and any added latency (e.g. logging) masked it. Detect the re-delivered duplicate via the record's own additionalAuditRefs instead. Every out-of-order write folded into a record records its {version, nodeId} ref under the same RocksDB-only condition as the cap, and the record is read with read-your-writes consistency, so on re-delivery the ref at version === txnTime is reliably present. Also removes a redundant audit read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6420f00 commit f469ae9

1 file changed

Lines changed: 19 additions & 12 deletions

File tree

resources/Table.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1864,8 +1864,7 @@ export function makeTable(options) {
18641864
// retained window are not layered in — but the authoritative full-copy record restores exact
18651865
// convergence. Because we stopped before reaching txnTime, the inline duplicate detection in
18661866
// the walk never ran; full-copy audit-replay re-delivers writes, and re-applying one would
1867-
// double-apply its commutative ops, so rule that out here with a single O(1) lookup at txnTime
1868-
// (RocksDB audit logs are keyed by version, and the cap is RocksDB-only).
1867+
// double-apply its commutative ops, so rule that out here before folding.
18691868
logger.warn?.(
18701869
'Out-of-order audit reconciliation exceeded depth cap; reconciling against most recent updates only',
18711870
{
@@ -1874,16 +1873,24 @@ export function makeTable(options) {
18741873
depth: walkSteps,
18751874
}
18761875
);
1877-
const duplicate = auditStore.get(txnTime, tableId, id, options?.nodeId);
1878-
if (
1879-
duplicate &&
1880-
duplicate.version === txnTime &&
1881-
precedesExistingVersion(
1882-
txnTime,
1883-
{ version: txnTime, localTime: txnTime, key: id, nodeId: duplicate.nodeId },
1884-
options?.nodeId
1885-
) === 0
1886-
) {
1876+
// Detect a re-delivered duplicate via the record's own additionalAuditRefs rather than an
1877+
// audit-log lookup at txnTime. Every out-of-order write folded into this record records its
1878+
// {version, nodeId} ref (added in the walk above, RocksDB-only — the same condition as this
1879+
// cap), and the record is read with read-your-writes consistency. The transaction-log query
1880+
// that a keyed audit lookup would use can lag a back-to-back re-delivery — the just-committed
1881+
// entry is not yet visible — which silently double-applied the commutative op (#1137).
1882+
// precedesExistingVersion(...) === 0 is the identity tie: same version AND same node (the
1883+
// local node is id 0, so an undefined options?.nodeId resolves to the same 0 the ref stored).
1884+
const alreadyApplied = existingEntry?.additionalAuditRefs?.some(
1885+
(ref) =>
1886+
ref.version === txnTime &&
1887+
precedesExistingVersion(
1888+
txnTime,
1889+
{ version: txnTime, localTime: txnTime, key: id, nodeId: ref.nodeId },
1890+
options?.nodeId
1891+
) === 0
1892+
);
1893+
if (alreadyApplied) {
18871894
write.skipped = true;
18881895
return; // duplicate write already applied
18891896
}

0 commit comments

Comments
 (0)