Skip to content

sync: a run's heartbeat never parks a conflict - #270

Merged
udaychandra merged 2 commits into
mainfrom
fix/run-heartbeat-never-conflicts
Sep 20, 2026
Merged

udaychandra merged 2 commits into
mainfrom
fix/run-heartbeat-never-conflicts

Conversation

@udaychandra

Copy link
Copy Markdown
Contributor

Field report

After upgrading to v0.44.1, Mady's first sail sync parked 22 conflicts, every one naming a single field: last_activity_at.

Root cause

last_activity_at is a run's heartbeat. RunStore.stampActivity writes it without a revision (by design — a revision per stamp would flood the change log), so an owning node's live row always runs ahead of what main last heard. That alone is harmless: only local moved → push.

It becomes a conflict when main also holds a stamp the node never recorded as its base — a push main committed whose acknowledgement the node lost (a round that died after its push). Base says T0, main says T1, the live row says T2: both sides moved the same field to different values → Conflict[last_activity_at]. Mady's node had days of failing rounds before v0.44; it met 22 such runs on its first good round.

Reproduced in RunSyncTest.aHeartbeatThatMovedOnBothSidesTakesTheLaterStampInsteadOfParkingAConflict (fails on main with exactly fields=[last_activity_at]).

Fix

  • Latest-wins fields. A SyncedStore declares instant-valued fields that only move forward (latestWinsFields(), none by default; RunStore declares last_activity_at). When both sides moved one, ConflictDetector merges to the later Instant (parsed, so …:41Z vs …:41.5Z compares correctly). The heartbeat still travels, so presence on main stays fresh. A real conflict beside it (e.g. status) still surfaces.
  • Self-heal. StoreReplica.adopt now closes whatever conflict is still open on the entity it just settled. This matters beyond tidiness: sail conflicts resolve writes the detection-time snapshot, so a parked conflict on an entity that has since reconciled cleanly is a loaded gun. A node that upgrades with these 22 parked heals on its next round.

Also

SessionVerbsTest failed once in the full run: it waited for a file to exist and then read it, but the shell creates the file before writing into it (expected room=design-talk but was <>). The child now writes beside the file and renames into place. Pre-existing, unrelated, fixed structurally.

Verification

mvn clean verify: 5,266 tests, all coverage gates met.

The test waited for the file to exist and then read it, but the shell creates
the file before it writes into it, so a loaded machine read it empty
('expected room=design-talk but was <>'). The child now writes beside the
file and renames it into place: existing means complete.
last_activity_at is stamped without a revision, so an owning node's live row
runs ahead of what main last heard. When main also holds a stamp the node
never recorded as its base (a push main committed whose acknowledgement the
node lost), both sides have moved the same field and the round parked a
conflict naming last_activity_at alone. A node that had days of failing
rounds met 22 of them on its first round after upgrading.

A store now declares its latest-wins fields: instant-valued fields that only
move forward. When both sides moved one, the detector merges to the later
instant instead of reporting a conflict; a real conflict beside it still
surfaces. RunStore declares last_activity_at.

Adopting an entity's settled state also closes whatever conflict is still
open on it. The disagreement no longer exists, and resolving it later would
write its detection-time snapshot over the settled row — so a node that
upgrades with such conflicts parked heals on its next round.
@udaychandra
udaychandra merged commit 298cbcd into main Sep 20, 2026
3 checks passed
@udaychandra
udaychandra deleted the fix/run-heartbeat-never-conflicts branch September 20, 2026 21:17
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