Skip to content

Order equal-version writes from unnamed origins, and keep the out-of-order walk from rewriting the head comparison - #3576

Merged
kriszyp merged 6 commits into
mainfrom
fix/precedes-existing-total-order
Oct 27, 2026
Merged

kriszyp merged 6 commits into
mainfrom
fix/precedes-existing-total-order

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 26, 2026 •

Copy link
Copy Markdown
Member
Ordering two same-time parcels in a canyon workshop

⊙ Problem

precedesExistingVersion orders 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

  • Distinct origins never tie: names compare, a named origin beats an unnamed one, two unnamed ones order by local id.
  • A keyed lookup by an unnamed origin's id finds its audit entries in the shared local log, and no other origin's.
  • The walk orders visited entries in a local variable; only the head's own entry settles a tie with the head.

⚖️ Alternatives

  • Map every origin to a name before applying — unnamed ids already exist in stored v4-era records, so the comparator must handle them anyway.
  • Fall back to local after 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

🤔 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).

✅ Verification

  • New 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.js expects two unnamed origins to both apply, and checks that one origin's re-delivery is still skipped; auditLog.test.js covers 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, LMDB replicationDirectiveAuthorization) 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.js 80/80, typestrip-runtime.test.mjs 38/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

Late 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
  • nit — 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-31 sets up and the unnamed ids that :12-13 define (the issue references can stay); (b) the second clause of unitTests/resources/replicatedRetryDuplicate.test.js:170-171 restates the tit

Not 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-1

Late Grok findings

1 surviving (highest nit) — Grok reviewed a303f2c
  • nit — resources/RocksTransactionLogStore.ts:377 — a numeric-id getRange({ log: id }) for an unnamed origin falls back to the whole local log. The result is unfiltered, so every local write and every other unnamed origin's entries come back under that id. Only getSync filters by node id (:222-225). In core, the production probe at `resources

Not 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

@kriszyp kriszyp added this to the v5.4 milestone Oct 26, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread resources/RocksTransactionLogStore.ts Resolved
Comment thread resources/Table.ts Resolved
@kriszyp
kriszyp force-pushed the fix/precedes-existing-total-order branch 3 times, most recently from 3617a7b to 52b5d2e Compare October 27, 2026 02:58
kriszyp and others added 6 commits October 27, 2026 06:03
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
Refs #3516, #3521

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-precedes-existing-ordering
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
@kriszyp
kriszyp force-pushed the fix/precedes-existing-total-order branch from 52b5d2e to a303f2c Compare October 27, 2026 12:25
@kriszyp
kriszyp marked this pull request as ready for review October 27, 2026 13:49
@kriszyp
kriszyp merged commit 7b87694 into main Oct 27, 2026
59 of 62 checks passed
@kriszyp
kriszyp deleted the fix/precedes-existing-total-order branch October 27, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant