Repository navigation
Conversation
…n backward A publish restamps the record it lands on with its own version, unconditionally. A replicated publish that arrives late, or a local publish on a node whose clock is behind a peer's, moves the record's version backward. A later late put then wins against a version it should lose to, and RocksDB readers that start from the record's version miss newer messages. The failing cases are wrapped as known failures so CI stays green; RUN_KNOWN_FAILURES=1 runs them plainly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-late-publish-version-regression-repro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-late-publish-version-regression-repro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-late-publish-version-regression-repro
ldt1996
pushed a commit
that referenced
this pull request
Nov 13, 2026
A publish set its record's version to its own, so a late replicated message (or one published behind a peer's clock) moved the version back and a later put, patch or delete that should lose could win on that node (#3637). The message now keeps the record's version and origin and is still logged and delivered; on RocksDB the record refs its entry. LMDB's out-of-order walk steps past a late message. Topic replays and getHistoryOfRecord follow refs to late entries only, bounded and pausing mid-transaction; a replayed late message is dropped from the queued live events by origin, key and version. Rebased onto #4337's replay: late messages are collected before the resume check and merged after the walked entries become events. Cases from #4360. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kriszyp
pushed a commit
that referenced
this pull request
Nov 15, 2026
A publish set its record's version to its own, so a late replicated message (or one published behind a peer's clock) moved the version back and a later put, patch or delete that should lose could win on that node (#3637). The message now keeps the record's version and origin and is still logged and delivered; on RocksDB the record refs its entry. LMDB's out-of-order walk steps past a late message. Topic replays and getHistoryOfRecord follow refs to late entries only, bounded and pausing mid-transaction; a replayed late message is dropped from the queued live events by origin, key and version. Rebased onto #4337's replay: late messages are collected before the resume check and merged after the walked entries become events. Cases from #4360. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
A publish sets a record's version to its own, with no ordering check:
_writePublishpasses itstxnTimetoupdateRecord, andrecordUpdaterstores it as the record's version. Puts and patches compare against that record version. A publish below the record's version moves it backward. This can be a late replicated publish or a local one on a node whose clock is behind a peer's.💡 Solution
No product code changes. New tests reproduce each consequence on the engine where it occurs, and check LMDB where it differs, applying replicated writes through the methods replication calls. Each failing case pins today's wrong result, so CI stays green until something changes it;
RUN_KNOWN_FAILURES=1asserts the correct result instead.🔎 Findings
Observed values, every case (offsets in ms from the case's base)
getHistoryOfRecordafter messages 1@100, 3@300, then late 2@200getHistoryOfRecordafter put@200 and late publish@150subscribe({ previousCount: 1 }), same messagessubscribe({ previousCount: 3 })subscribe({ startTime: 250 })On RocksDB,
getHistoryOfRecordstarts at the record's head and follows a previous link only below its window. The late entry links up to 300, so the walk stops. AstartTimebackfill walks only when the head is abovestartTime. A count backfill follows links to the end, which is whypreviousCount: 3finds all three.Fix sketch: restamp the record only forward
In
_writePublish's commit, order the publish against the record withprecedesExistingVersion. When it precedes, log and deliver it but keep the record's version and node id (recordUpdateralready takes a separate audit version,options.recordVersion). Then a replicated publish strictly above the record's version cannot be a re-delivery, so #4339's log probe is needed only for late and tied ones.What the forward-only restamp would break
getHistoryOfRecordand thepreviousCount/startTimebackfills would miss the late message instead of the newer ones they miss today. Keeping it reachable means listing its log key in the record'sadditionalAuditRefs, as out-of-order puts do.resolveAuditHeadstill picks the entry whose version equals the record's, so the head stays right. The cost is one ref per late message until the next in-order write, after which it survives only aspreviousAdditionalAuditRefs.getRecordAtTime) is unaffected, since a message does not change the value. Anything that lists messages from the chain is affected.🤔 Known failures pinned in CI rather than skipped
Instead of
it.skip: a fix, or any other change here, turns CI red, so whoever changes the behavior updates the test. The cost is green tests titled with the correct behavior.🤔 Unit-level replication, not a two-node cluster
The tests call
_writePublish/_writeUpdatewith a source-apply context, as #4339's test does: the methods replication uses. These orders on two real nodes are inferred, not run.✅ Verification
RUN_KNOWN_FAILURES=1, 9 fail on RocksDB and 5 on LMDB, with the table's values.test:unit:resources:core:a-n(this file's shard): 3434 passing, 0 failing. Lint, prettier andcheck:unit-partitionclean.test:unit:mainand integration; no product code changed.Refs #4339
🤖 Generated by Claude Opus 5.5 (Claude Code); posted via @kriszyp.
Release target: main — test-only, no product change
Related PRs: none found
Complexity: easy
Deferred findings
bdb95d67e2deef59nit test-coverage —unitTests/resources/latePublishVersionRegression.test.js— LMDB runs h1/h2 as plain it(), assuming LMDB history parity — NOT FILED8b53ed623607d829nit maintainability —unitTests/resources/latePublishVersionRegression.test.js— suggest a comment explaining why previousCount: 3 stays green — NOT FILEDdeb63d6a8805443dnit maintainability —unitTests/resources/latePublishVersionRegression.test.js— suggest cross-referencing p1 from h1 so p1 passing is not read as h1 should pass — NOT FILEDefd63e8b68bfdb87nit maintainability —unitTests/resources/latePublishVersionRegression.test.js— added comments narrate code or describe current pre-fix behavior — NOT FILEDOrigin — the dispatch brief this PR was written from
Reproduce: a late replicated publish moves a record's version backward on RocksDB, letting an older replicated put win
Test-only investigation in HarperFast/harper. Change no product code; deliver a draft PR containing the reproducing test(s) and a findings write-up in the body.
Hypothesis (from code reading, not yet confirmed):
Table.ts_writePublishcommits throughupdateRecord(id, value, existingEntry, txnTime, …, 'message').recordUpdater(resources/RecordEncoder.ts, theif (isRocksDB) timestampNextEncoding = newVersionbranch) stores the publish's txnTime as the record's single version and transaction-log reference, unconditionally.precedesExistingVersioncheck. So a replicated publish that arrives late, with a timestamp below the record's current version, moves the record's version backward.Consequences to test:
Expected, if the hypothesis holds: this node ends with C at version 170, while last-writer-wins says B at 200 must stay. Also check the reverse order and a timestamp tie.
Simulate replicated applies the way
unitTests/resources/replicatedPublishRedelivery.test.json #4339 does: a source-apply context with an origin nodeId and timestamp. A two-node harper-pro cluster test is not needed unless the unit-level simulation cannot reach the path.Write the tests so they fail on main if the bug is real, and mark them as expected failures so CI stays green. In the PR body, state whether each consequence is confirmed, with the observed versions and values. Then sketch a fix: a publish restamps the record only forward (keep the maximum version) while still logging and delivering the late message. Say what that would break, especially history walks and the audit previous-version chain. Context: harper#4339 dedupes re-sent publishes by probing the origin's log on every replicated publish, because the record's version can move backward; a forward-only restamp would let a version check skip that probe for in-order messages.
Acceptance
A draft PR with tests that fail on main for each confirmed consequence, marked as expected failures, or a clear explanation of why a hypothesis does not hold. The PR body includes the fix sketch and its risks.
Dispatch: task
harper-late-publish-version-regression-repro· queued by kris-session · ran by claude/opus/high · worker kris-mac-m3-fleet-18b9c6Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; declined=muse; rounds=3; full=2 @ 1d69f5d
Review-Attention: skim ~2m (decisions: pin-exact-wrong-result, private-write-fixture) @ 1d69f5d