Skip to content

Commit 7c0ef55

Browse files
authored
Merge pull request #1147 from HarperFast/fix/1137-commutative-op-double-apply
fix(table): reliably skip a re-delivered out-of-order commutative op (#1137)
2 parents 81efaaf + 3e0e3fa commit 7c0ef55

1 file changed

Lines changed: 32 additions & 2 deletions

File tree

resources/Table.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,32 @@ export function makeTable(options) {
17601760
// of the updates to the record to ensure consistency across the cluster
17611761
// TODO: can the previous version be older, but even more previous version be newer?
17621762
if (audit) {
1763+
// A re-delivered out-of-order write (full-copy audit-replay re-delivers writes) must not have
1764+
// its commutative ops re-folded. additionalAuditRefs is the record's own list of folded
1765+
// out-of-order versions, read with read-your-writes consistency, so this skips the duplicate up
1766+
// front — before the audit-log walk below, which can miss it: the walk stops at the depth cap, or
1767+
// breaks early on a not-yet-visible audit entry, before reaching txnTime, and the keyed
1768+
// transaction-log lookup it would otherwise use can lag a back-to-back re-delivery (that lag
1769+
// silently double-applied the increment — #1137). This covers the re-delivery while the ref is
1770+
// still on the record; a later in-order write rewrites the record and drops the ref (it survives
1771+
// only as previousAdditionalAuditRefs on the audit log), so that case falls back to the
1772+
// best-effort keyed lookup in the capped block below — see #1148. precedesExistingVersion(...)
1773+
// === 0 is the identity tie: same version AND same node (the local node is id 0, so an undefined
1774+
// options?.nodeId resolves to the same 0 the ref stored).
1775+
if (
1776+
existingEntry.additionalAuditRefs?.some(
1777+
(ref) =>
1778+
ref.version === txnTime &&
1779+
precedesExistingVersion(
1780+
txnTime,
1781+
{ version: txnTime, localTime: txnTime, key: id, nodeId: ref.nodeId },
1782+
options?.nodeId
1783+
) === 0
1784+
)
1785+
) {
1786+
write.skipped = true;
1787+
return; // out-of-order write already folded into this record
1788+
}
17631789
// incremental CRDT updates are only available with audit logging on
17641790
let localTime = existingEntry.localTime;
17651791
let auditedVersion = existingEntry.version;
@@ -1891,8 +1917,12 @@ export function makeTable(options) {
18911917
// retained window are not layered in — but the authoritative full-copy record restores exact
18921918
// convergence. Because we stopped before reaching txnTime, the inline duplicate detection in
18931919
// the walk never ran; full-copy audit-replay re-delivers writes, and re-applying one would
1894-
// double-apply its commutative ops, so rule that out here with a single O(1) lookup at txnTime
1895-
// (RocksDB audit logs are keyed by version, and the cap is RocksDB-only).
1920+
// double-apply its commutative ops. A re-delivered out-of-order write is already ruled out by
1921+
// the additionalAuditRefs check at the top of this block; this keyed lookup is the best-effort
1922+
// guard for the remaining case — a re-delivered write that was originally in-order (so it left
1923+
// no ref) and is now deeper than the cap. It is best-effort because the transaction-log lookup
1924+
// can intermittently miss an entry under load (tracked separately); the authoritative full-copy
1925+
// record still restores exact convergence.
18961926
logger.warn?.(
18971927
'Out-of-order audit reconciliation exceeded depth cap; reconciling against most recent updates only',
18981928
{

0 commit comments

Comments
 (0)