Skip to content

Fix permanent three-node sync divergence; bound the stress test by rate - #34

Merged
myobie merged 2 commits into
mainfrom
fix/sync-version-leapfrog
Aug 3, 2026
Merged

Fix permanent three-node sync divergence; bound the stress test by rate#34
myobie merged 2 commits into
mainfrom
fix/sync-version-leapfrog

Conversation

@myobie

@myobie myobie commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Two commits, one review.

Engine fix. Materialization no longer stamps the origin's mtime. That stamping (mine, a5bfde2) existed so the scan cache (ccddbee, also mine) would hit on materialized files, but it made mtime a value shared across nodes — the one assumption that cache cannot survive. Two nodes contending on a key produce equal-size entries that can collide on size and mtime with different content; the scan then reports bytes the file does not hold, the republish branch reads the real bytes, and stale content is republished under local authorship. Both sides do it and versions leapfrog forever.

Evidence, Linux CI run 30814462024 attempt 4: twenty-four consecutive alternating republishes, v44→v67, never converging, every line with prior manifest hash equal to the scanned hash but a different hash after. journal_match/journal_committed true throughout, which ruled out the daemon-write journal.

Cost, asserted rather than described: one re-read per materialized file. Untouched local files still hit the cache.

Mode-1 test correction. The continuous-mutation stress is now bounded by rate, not fixed counts, preserving the original relationships exactly (4 reconciles/s, scans and persists at 112/36 and 76/36 of it). Validated against all four observed runs including both failures.

Residual risk asserted, not hidden: the cache still trusts a size+mtime collision arriving by another route. Closing that needs a content-identity mechanism, deliberately deferred.

myobie added 2 commits August 3, 2026 14:28
The fixed counts were duration-sensitive, which made this test flaky rather than
strict. The writers perform a fixed NUMBER of revisions with a sleep between them,
so a loaded machine stretches the burst, elapses more coalescing windows, and does
proportionally more legitimate work.

Measured: it passes locally at 30 to 34 reconciles against an old cap of 36, a
margin of two, while Linux CI produced 54 in 15.57s and 43 in 16.14s. Every one of
those runs, both failures included, stayed under 4 reconciles per second. So the
old caps were failing runs whose actual amplification was fine, and raising them
would only move the cliff.

The ceilings keep the original relationships exactly: the reconcile rate stays at
4 per second and the scan and persist ceilings are the old caps expressed against
that same rate, 112/36 and 76/36 of it, which is the derivation the previous
comment already described. No new number is invented. Validated against all four
observed runs including both failures.

This is the mode-1 half of the review. The engine defect it was masking is next.
Fixes the permanent three-node divergence. The cause was mine, from two commits
earlier in this line: ccddbee added a scan cache that reuses a recorded hash when
size and both mtime components match, and a5bfde2 made materialization stamp the
ORIGIN's mtime so that cache would hit on materialized files.

Together they turned mtime from a local write timestamp into a value shared across
nodes, which is the assumption the cache cannot survive. Two nodes contending on
one key produce entries of equal size that can carry equal mtimes with different
content. The scan then reuses the recorded hash and reports content the file does
not hold; the republish branch reads the real bytes, sees a different hash,
concludes a user edited the file, and republishes stale content under local
authorship. Both sides do it and versions leapfrog forever.

Measured on Linux CI run 30814462024 attempt 4: twenty-four consecutive
republishes alternating between two nodes, every line showing the prior manifest
hash equal to the scanned hash yet a different hash afterwards, v44 through v67,
never converging. journal_match and journal_committed were true throughout, which
is what ruled out the daemon-write journal and pointed here instead.

A materialized file now carries the local write time, so the cache misses on it
and the next scan reads the real bytes. Untouched local files still hit the cache,
which is where the benefit actually was. The cost is one re-read per materialized
file, asserted directly rather than described.

Proofs: materialization no longer manufactures an mtime collision; an untouched
file still gets a cache hit and is not re-read; a real user edit still republishes
with local authorship and a higher version. The collision test also asserts the
residual risk this does NOT close, that the cache still trusts a collision arriving
by some other route, since closing that needs a content-identity mechanism that was
deliberately deferred.

The test that previously asserted the stamping worked now asserts the opposite and
carries the reason, because that assertion was the defect.

Carries the mode-1 rate correction in the same review.
@myobie
myobie merged commit 00c528f into main Aug 3, 2026
2 checks passed
@myobie
myobie deleted the fix/sync-version-leapfrog branch August 4, 2026 00:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant