Skip to content

Persist gossip bookmarks - #65

Draft
plotnick wants to merge 10 commits into
wire-breakfrom
gossip-persistence
Draft

Persist gossip bookmarks#65
plotnick wants to merge 10 commits into
wire-breakfrom
gossip-persistence

Conversation

@plotnick

Copy link
Copy Markdown
Collaborator

Just identity bookmarks so far, but full rumors set persistence coming soon!

plotnick and others added 10 commits August 27, 2026 10:47
A sled that joins an established universe receives its entire history,
and until now executed every retained job as its causal chain became
ready. The gossip manager now publishes the causal frontier of the set
received at join alongside the rumors handle, and the state machine
executes only jobs that arrive as strict causal descendants of that
frontier.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
A sled that reboots mid-job leaves the rack believing the job runs
forever: the executor died with the process, so no stop event ever
comes. Now, when replayed history at a universe join shows jobs
running on our own baseboard that this incarnation is not actually
running, we gossip an error event for each with the new
ProcessError::Interrupted, closing them out honestly for every seat
in the rack (sush#9).

The scan runs only at local quiescence, after draining every message
already delivered, so a job whose terminal event is present is never
falsely interrupted. A stop that is still in flight elsewhere in the
rack can race the declaration; the audit trail then carries both, and
honestly. Jobs this incarnation really is running when its universe
migrates are excluded: they continue, and their events land in the
new universe.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
A rumors bookmark persists a peer's identity so a restarted sled
reclaims it instead of stranding it. We owe rumors raw byte storage
with two guarantees: stores commit atomically, and a load never
returns a record older than the newest store we acknowledged, since a
stale record claims coverage the identity already transmitted past
and corrupts causality on reclamation, whereas a lost record merely
strands.

Records live in a small sequence-numbered envelope, one file per boot
M.2 slot. Loads read every slot and the highest sequence wins,
tolerating boot-device flips and disk swaps; stores go only to the
winning slot, so one disk's health is in the write path rather than
two. A BookmarkSource hands out one handle per peer: minting a handle
supersedes all earlier ones, so a straggling store from an abandoned
universe cannot clobber its successor's record. Shed handles persist
nothing, for peers that must keep gossiping after their storage
failed, and the null source persists nothing at all, for the
standalone server and tests.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
Every gossip peer now carries a bookmark handle. The seed's is
attached at creation, where a pristine peer costs nothing; a
migration mints a fresh handle for the joined peer, superseding the
abandoned universe's. Identities recorded by previous incarnations
are reclaimed at the first gossip after a migration returns us to
the universe that knew them.

Persistence must never cost availability. A bookmark failure inside
a session aborts that session before any wire traffic, so a seed on
bad storage could never even learn it lost dominance: probe the
storage at seeding and shed the bookmark up front. A join whose
identity cannot be persisted likewise sheds and gossips on. Both
degradations merely strand identities, which is what every restart
did before this commit. A store that fails after a successful join
still stops that sled's gossip until its next migration or restart;
the planned rumors API for shedding a live peer's bookmark is the
remaining fix.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
The adversarial review found the orphan scan drawing candidates from
every job running on our baseboard, so a replayed straggler arriving
while a live job ran would declare that live job interrupted, break
its attachment, and discard its eventual result. Candidates now come
only from start events that themselves arrived as replayed history:
a job this incarnation started can never be a candidate, no matter
when the scan runs.

Three more review findings ride along. A genuine stop and an
interrupted declaration now converge to the real result in either
arrival order (errors no longer displace terminal statuses, and a
stop supersedes an interrupted error), instead of the interrupt
winning both races. A job we declared interrupted no longer counts
as a survivor at the next universe swap, so back-to-back migrations
re-declare it rather than exempting it forever. And replayed
messages no longer mint local cancelled statuses or re-gossip
concurrent-session errors, which grew the set a little more on
every rejoin by every sled.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
The adversarial review found the one write path the atomic rename
does not serialize: a store future dropped at its await (a migrating
manager aborts session drivers; a join can time out) detaches the
blocking write, whose rename then lands on top of a newer record
that already returned Ok. Renames now go through a commit fence: a
write lands only if its sequence number exceeds everything this
process has committed, so a straggler loses instead of clobbering.

The envelope also gains a digest over the sequence number and
record, since the whole safety argument rides on an integer that
was previously trusted straight off the disk. Loads no longer
regress the in-memory sequence reservation or serve superseded
handles, the probe proves a slot writable by writing instead of
guessing from directory metadata, and a bookmark failure inside a
gossip session now warns loudly: it stops every later session at
the persist gate, so unlike routine link churn it must not hide at
debug level.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
A job whose process died with a previous incarnation but whose
bookkeeping says it still runs is exactly what Unix calls a zombie,
and recording its death is reaping. Rename replayed_started,
orphaned_jobs, and interrupt_orphans accordingly, and sweep the
recently added comments for style: no more spliced clauses, one home
per argument, and the supersede vocabulary instead of fences.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
The ipcc feature gate merged upstream as 98615b86, identical in
content to the branch rev we carried; the pin now names the commit
on sprockets main.

Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5 <noreply@anthropic.com>
@plotnick plotnick changed the title Gossip persistence Persist gossip bookmarks Aug 31, 2026
@plaidfinch

Copy link
Copy Markdown
Collaborator

This approach for piggybacking on bookmarks to determine which jobs to execute during replay has a number of issues, which admittedly are probably not adequately called out in the docs for bookmarking.

At a high level:

  1. Bookmarking should be thought of as party identifier loss recovery exclusively, and not as a high-water-mark that reliably and promptly recovers the last-seen version. A bookmark cannot reclaim the stored prior party identity until it has observed through gossip all the messages the prior party originated — until then, the bookmark is inert. At no point does a bookmark itself cause the version of the peer to be updated before the peer organically learns information through gossip, since doing so would lead to causality violation and contagious false redaction; it can only cause the party of the peer to join with the prior identity after the peer learns enough that it can be assured it will not retread previously-issued causal versions if it stamps them with the persisted identity.
  2. In the sush architecture, there is a distinction between message receipt and message execution: just because you know about a job intended for you to execute, does not mean you yet have executed it — or even that you could execute it even in principle, since the linear hash-chain sequencing of jobs need not be monotonic with the causal versions of those jobs (if, for example, they are submitted through multiple endpoints, or even merely out of order). Nothing in the protocol enforces that the partial ordering of causal message versions agrees with the total ordering of jobs, so a job A whose start message causally precedes another job B's start message may itself be sequenced after job B from the sush state machine's perspective. Nothing in any level of any protocol prevents this, nor need it. Because these two notions of ordering are fundamentally unrelated, it's not possible to use a message version as a high-water-mark to indicate which jobs have been executed, not even if you were to augment the implementation to close the "message receive" vs. "message execute" gap by recording the last-executed message's version: in some cases, this version would go backwards!

As implemented in this PR, I believe sled restarts can trigger both arbitrary skipping of jobs (if the job is received-but-not-executed, it will be skipped upon restart, if the bookmark is absorbed on bootstrap), as well as arbitrary replay of jobs (if the bookmark fails to be absorbed temporarily due to a network partition withholding prior-sent messages (this is necessary for causal safety!), the high-water-mark as witnessed by the in-memory peer will be too low, and replay will fail to filter jobs it already did run).

Consequently, I believe the correct high-water-mark for tracking job execution and preventing local replay of already-executed jobs during resynchronization of a restarted sled is the job ID itself. Because jobs have a total ordering defined by their hash chain, this alone gives a unique pointer indicating where to resume. This high-water-mark should not be embedded in the rumors bookmark, which is (and must be) automatically read and written during the course of gossip. Adding a second piece of application state to that file (without a filesystem locking mechanism or similar) presents an acute opportunity for an A-B-A problem where the read/write to update the job high-water-mark would inadvertently roll back a concurrent update to the rumors bookmark content itself. This would be a difficult-to-replicate data race that would lead to non-deterministic arbitrary corruption of the rumor set (since bookmark rollback of any kind if causally unsafe).

This approach does have a caveat: all actions other than job start must be idempotent from the perspective of side effects on the host — let's call this host idempotency — because if we only track the last-started job ID to prevent replay of job starts, we cannot use this to bootstrap replay protection for other message kinds. In particular, this is because other message kinds are not linearly sequenced like job starts are: for example, two job stop messages, when received by sush, may execute in arbitrary order relative to one another, and relative to pending job starts or other actions.

As a final note, we should also consider a second property of our messages; let's call this rumor idempotency. During replay after a restart, with the corrected replay-filtering mechanism, we can prevent jobs from starting, but we don't have a mechanism for a singular high-water-mark for other actions, like job stop. If these host-idempotent actions themselves emit new messages back into the rumor set, then every sled restart will generate a storm of duplicative acknowledgement messages during replay, which, in the worst case, is multiplicatively amplified if those messages themselves could possibly generate responses. As such, I'd like to propose the rule that if an action has no host effect (i.e. stopping a job that was already stopped), then it should not inject more messages back into the rumor set. Are there any actions which can't or shouldn't be made to fit this? Do you have thoughts about how we can avoid this problem in other ways I'm not thinking of?

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.

2 participants