Skip to content

Commit 626f818

Browse files
heskewclaude
authored andcommitted
rework: detect re-delivered out-of-order duplicate up front (#1137)
Per cross-model review (Codex): the prior commit replaced the capped-path keyed audit lookup with an additionalAuditRefs check, which dropped duplicate detection for a write re-delivered on the main (in-order) previousVersion chain. Instead: - Move the additionalAuditRefs duplicate check to the TOP of the out-of-order block, so a re-delivered out-of-order write is skipped reliably before the audit walk — immune to the walk stopping at the #1114 depth cap or breaking early on a not-yet-visible audit entry (both let the prior keyed lookup miss the duplicate). - Restore the capped-path keyed auditStore.get lookup unchanged as the best-effort guard for a re-delivered originally-in-order write (which leaves no ref); its residual unreliability under load is tracked in #1148. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f469ae9 commit 626f818

1 file changed

Lines changed: 42 additions & 19 deletions

File tree

resources/Table.ts

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,6 +1733,32 @@ export function makeTable(options) {
17331733
// of the updates to the record to ensure consistency across the cluster
17341734
// TODO: can the previous version be older, but even more previous version be newer?
17351735
if (audit) {
1736+
// A re-delivered out-of-order write (full-copy audit-replay re-delivers writes) must not have
1737+
// its commutative ops re-folded. additionalAuditRefs is the record's own list of folded
1738+
// out-of-order versions, read with read-your-writes consistency, so this skips the duplicate up
1739+
// front — before the audit-log walk below, which can miss it: the walk stops at the depth cap, or
1740+
// breaks early on a not-yet-visible audit entry, before reaching txnTime, and the keyed
1741+
// transaction-log lookup it would otherwise use can lag a back-to-back re-delivery (that lag
1742+
// silently double-applied the increment — #1137). This covers the re-delivery while the ref is
1743+
// still on the record; a later in-order write rewrites the record and drops the ref (it survives
1744+
// only as previousAdditionalAuditRefs on the audit log), so that case falls back to the
1745+
// best-effort keyed lookup in the capped block below — see #1148. precedesExistingVersion(...)
1746+
// === 0 is the identity tie: same version AND same node (the local node is id 0, so an undefined
1747+
// options?.nodeId resolves to the same 0 the ref stored).
1748+
if (
1749+
existingEntry.additionalAuditRefs?.some(
1750+
(ref) =>
1751+
ref.version === txnTime &&
1752+
precedesExistingVersion(
1753+
txnTime,
1754+
{ version: txnTime, localTime: txnTime, key: id, nodeId: ref.nodeId },
1755+
options?.nodeId
1756+
) === 0
1757+
)
1758+
) {
1759+
write.skipped = true;
1760+
return; // out-of-order write already folded into this record
1761+
}
17361762
// incremental CRDT updates are only available with audit logging on
17371763
let localTime = existingEntry.localTime;
17381764
let auditedVersion = existingEntry.version;
@@ -1864,7 +1890,12 @@ export function makeTable(options) {
18641890
// retained window are not layered in — but the authoritative full-copy record restores exact
18651891
// convergence. Because we stopped before reaching txnTime, the inline duplicate detection in
18661892
// 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 before folding.
1893+
// double-apply its commutative ops. A re-delivered out-of-order write is already ruled out by
1894+
// the additionalAuditRefs check at the top of this block; this keyed lookup is the best-effort
1895+
// guard for the remaining case — a re-delivered write that was originally in-order (so it left
1896+
// no ref) and is now deeper than the cap. It is best-effort because the transaction-log lookup
1897+
// can intermittently miss an entry under load (tracked separately); the authoritative full-copy
1898+
// record still restores exact convergence.
18681899
logger.warn?.(
18691900
'Out-of-order audit reconciliation exceeded depth cap; reconciling against most recent updates only',
18701901
{
@@ -1873,24 +1904,16 @@ export function makeTable(options) {
18731904
depth: walkSteps,
18741905
}
18751906
);
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) {
1907+
const duplicate = auditStore.get(txnTime, tableId, id, options?.nodeId);
1908+
if (
1909+
duplicate &&
1910+
duplicate.version === txnTime &&
1911+
precedesExistingVersion(
1912+
txnTime,
1913+
{ version: txnTime, localTime: txnTime, key: id, nodeId: duplicate.nodeId },
1914+
options?.nodeId
1915+
) === 0
1916+
) {
18941917
write.skipped = true;
18951918
return; // duplicate write already applied
18961919
}

0 commit comments

Comments
 (0)