Repository navigation
Order equal-version writes from unnamed origins, and keep the out-of-order walk from rewriting the head comparison - #3576
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces deterministic conflict resolution and ordering for equal-version writes from distinct origins, particularly handling unnamed or unmapped origins by ordering them by local ID and routing their lookups to the local log. It also refines the out-of-order walk's head comparison to prevent older writes from incorrectly overwriting newer heads. The review feedback highlights two important issues: first, in RocksTransactionLogStore.ts, the sharedLog check in getSync should be adjusted to prevent cross-talk of remote unnamed origins when querying local logs (nodeId === 0); second, in Table.ts, the comparison of unnamed origins in precedesExistingVersion should default nodeId and existingNodeId to 0 to avoid comparison bugs when they are undefined or null.
3617a7b to
52b5d2e
Compare
precedesExistingVersion compared node names looked up from the id mapping, so an origin with no mapped name (v4-era data, or a relayed origin before its name arrives) broke the tie order: a named origin against an unnamed one returned -1 in both directions, so each node kept whichever write arrived first, and two unnamed origins tied at 0, which callers read as a re-delivery and dropped. Distinct ids now never tie: names compare when both exist, a named origin ranks above an unnamed one, and two unnamed origins order by local id. An unnamed origin's audit entries are filed in `local`, but a keyed lookup by its numeric id read only the id's own (absent) log, so the out-of-order walk could not see them. getRange now reads `local` for such an id, and getSync on that shared log returns only the requested origin's entries and stops at the first entry outside the requested transaction. Fixes #3516 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-precedes-existing-ordering
… the head _writeUpdate computes precedesExisting against the record's head, and after the out-of-order audit walk three steps read it: the stale-put skip, the head slot in additionalAuditRefs, and recordNodeId. The walk overwrote it with its comparison against whichever visited entry shared the write's version. A walk that reached an older entry at the write's own version from a lower-named origin turned -1 into +1, so an older put revalidated a newer invalidation and an older patch stamped its origin on the newer head. That also let a re-delivered head re-apply its increment after a three-way tie. The walk now orders each entry in a local precedesAudited. Only the head's own entry, in a same-origin tie, resolves a 0 head comparison. Fixes #3521 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-precedes-existing-ordering
… by number An unnamed origin's entries sit in `local` under minted keys, so the two received-key dedup lookups cannot hit there, and now that its numeric id reads `local` each miss would scan to the end of the log; skip them as main did. receivedIdentityRefs still dedups that write class, now pinned by a same-origin re-delivery test. Refs #3516, #3521 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-precedes-existing-ordering
…sign-doc budget resources/DESIGN.md is over its 1000-line budget on main; moving the numeric-selector and equal-version notes into the existing audit-identity bullet list and unwrapping two adjacent bullets stays within the budget. Refs #3516, #3521 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Gemini <noreply@google.com> Dispatch-Task: harper-precedes-existing-ordering Dispatch-Task: pr-maint-cee63837db7efadaa458b1c4acdeaff9
52b5d2e to
a303f2c
Compare
⊙ Problem
precedesExistingVersionorders replicated writes at one version. Origins with no node name (v4-era, or relayed before the name arrives) broke it:The out-of-order audit walk overwrote the write's comparison with the head (#3521). An older write could then revalidate or restamp a newer head.
🤔 Is the requirement warranted at this size?
Yes. Both silently diverge or lose writes. The fix stays in the comparator, the walk and the audit-log read; no format or API change.
💡 Solution
locallog, and no other origin's.⚖️ Alternatives
localafter any named-log miss (keeps entries readable after an id later gets a name) — rejected because it adds a second log scan to every first delivery of an out-of-order write.🤔 Is a per-node order for two unnamed origins acceptable?
Ids are minted per database, so two nodes can keep different winners for the same two unnamed origins. That beats dropping a write; #3563 tracks a cross-node identity.
🔧 Changes
resources/Table.ts: the comparator's total order over distinct ids; the walk's localprecedesAuditedand the head-only assignment; the received-key dedup lookups skip an unnamed origin, whose entries sit under minted keys, so a miss would scan to the end oflocal.recordNodeIduse it.resources/RocksTransactionLogStore.ts:getRangereadslocalfor an unnamed numeric id;getSyncchecks the transaction key first and, on that shared log, the origin.🤔 Should two unnamed ids count as two origins?
Yes, as harper-pro's replication already does. If one origin ever arrived under two unnamed ids, an increment would apply twice. Treating them as one re-delivery dropped real writes from distinct origins (#3516).
resources/DESIGN.md: the numeric-selector note updated and a new note on equal-version order and the head comparison, both now bullets in the audit-identity list so the file stays within its 1000-line budget;DESIGN.mddrops the selector's own index line.✅ Verification
equalVersionOriginOrder.test.js: 7 tests on RocksDB, 6 on LMDB. Against an origin/main build, 6/7 fail on RocksDB and 4/6 on LMDB; all pass with the fix.replicatedRetryDuplicate.test.jsexpects two unnamed origins to both apply, and checks that one origin's re-delivery is still skipped;auditLog.test.jscovers an unnamed id's keyed read, a wrong-origin miss and no log creation.test:unit:resources: RocksDB 4906 passing, LMDB 3715 passing. Its 1–3 failures (subscribeReadGateCallerImpact, LMDBreplicationDirectiveAuthorization) fail identically on origin/main.test:unit:main: 8078 passing. 24 HTTP socket hang-ups and TypeStrip aborts under load; those files pass alone (static.test.js80/80,typestrip-runtime.test.mjs38/38).test:integration:all: not run locally; other worktrees' Harper nodes held every loopback address. CI runs it.PR QA: Do equal-version writes from named and unnamed origins converge when applied concurrently through a real source subscription and survive a restart? — queued @ e8d80c2, qa-pr-harper-3576-e8d80c2 (since then: rebase, comment trims)
Docs: none needed — no API or config change.
Fixes #3516
Fixes #3521
🤖 Generated by Claude Opus 5.5 (Anthropic, dev-agent); posted via @kriszyp.
🤖 Generated with Claude Code
Release target: v5.4 — the walk and dedup sites come from #3510 (v5.4); the older comparator gap has no customer report asking for a backport
Related PRs: #3510 overlaps (this fixes residuals of its tie handling), #3453 overlaps (both edit replicatedRetryDuplicate.test.js), #3506 overlaps (audit:false equal-version patches use precedesExisting <= 0), #3438 independent (DESIGN.md only)
Complexity: complicated
Deferred findings
3276d14a7dff6d9eminor correctness —resources/RocksTransactionLogStore.ts— getSync does not filter local entries by nodeId for id 0 — filed getSync on the local transaction log does not filter by origin for node id 0 #3564d2bcca5d9f70c979nit performance —resources/RocksTransactionLogStore.ts— getSync calls logById for sharedLog and getRange repeats it (remainder of the bundled headIdentity finding) — filed getSync on the local transaction log does not filter by origin for node id 0 #35647da73a190543fc65nit data-integrity —resources/DESIGN.md— unnamed origins ordered by per-database id can converge differently across nodes — filed Two unnamed origins tied at one version can resolve differently on different nodes #3563Late Grok findings
none surviving — Grok reviewed c8a64e7
No finding survived adjudication.
Not standing: none. Grok ran 19m (19m from launch to report); the fold adjudication (
cursor-grok@c8a64e73bc0b-62601a9d, claude) took 1m.Late Grok findings
1 surviving (highest nit) — Grok reviewed 3617a7b
unitTests/resources/equalVersionOriginOrder.test.js:1— new comments repeat what nearby code already shows: (a) the header line repeats the node-name order that:28-31sets up and the unnamed ids that:12-13define (the issue references can stay); (b) the second clause ofunitTests/resources/replicatedRetryDuplicate.test.js:170-171restates the titNot standing: 1 resolved. Grok ran 13m (13m from launch to report); the fold adjudication (
cursor-grok@3617a7b7d518-a5b51e78, claude) took 1m.Origin — the dispatch brief this PR was written from
Two equal-version ordering defects in the same function family; fixes #3516 and #3521 (two commits). (1) precedesExistingVersion (resources/Table.ts): with one node name undefined it returns -1 in both directions (not antisymmetric, untracked); with both undefined it returns 0 for distinct node ids (#3516). Define a total order for distinct ids: compare names when both defined; a defined name beats an undefined one; both undefined -> compare node ids numerically; return only 1/-1 for distinct ids. Also RocksTransactionLogStore.getRange resolves a numeric options.log via logById only and returns [] when absent, while logForOrigin/isLocalOrigin file unmapped origins' entries in this.log, so keyed lookups in resolveAuditHead and the walk miss an unmapped origin's own entry: when a numeric log has no log and isLocalOrigin is true, read this.log. (2) The _writeUpdate audit walk assigns the outer precedesExisting inside
if (auditedVersion === txnTime); that value is later consumed by the stale-put skip, head-slot additionalAuditRefs ordering and recordNodeId. Besides #3521's same-version cases this also fires cross-version: txnTime < head and the walk reaches an entry at exactly txnTime from a lower-named origin flips <0 to >0 and lets a stale put overwrite the newer record. Use a local (precedesAudited) for walk control flow; only the head comparison may set precedesExisting (when it was 0).Acceptance
Tests on rocksdb and lmdb: mapped vs unmapped origins at the same version converge to one winner in both arrival orders (put/put, put/patch); two unmapped origins both apply counter adds; unmapped patch then mapped put reconciles via audit lookup; head (t2,D) over (t1,A) with incoming put (t1,C), C>A, preserves t2; patch (t1,B) under (t2,D) then (t2,C) patch -> D's field wins; 3-way tie then head re-delivery does not double-apply. Show each new test failing on origin/main sources (forced rebuild) before the fix.
Dispatch: task
harper-precedes-existing-ordering· queued by finding-triage/wave-40 · ran by claude/opus/high · worker kzyp-xps-1Late Grok findings
1 surviving (highest nit) — Grok reviewed a303f2c
resources/RocksTransactionLogStore.ts:377— a numeric-idgetRange({ log: id })for an unnamed origin falls back to the wholelocallog. The result is unfiltered, so every local write and every other unnamed origin's entries come back under that id. OnlygetSyncfilters by node id (:222-225). In core, the production probe at `resourcesNot standing: 1 dropped-factually-wrong. Grok ran 19m (19m from launch to report); the fold adjudication (
cursor-grok@a303f2c5022d-84d96865, claude) took 1m.Review-Coverage: authored=gemini; ran=cursor-composer,codex,cursor-grok,claude; late=cursor-grok:adjudicated; adjudicated=domain; declined=muse; rounds=8; full=3 @ a303f2c
Review-Attention: study ~15m (critical: RocksTransactionLogStore.ts, Table.ts; decisions: named-outranks-unnamed, unnamed-ordered-by-local-id, local-fallback-in-getrange, no-fallback-for-later-named-ids) @ a303f2c