Persist gossip bookmarks - #65
Conversation
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>
|
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:
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? |
Just identity bookmarks so far, but full rumors set persistence coming soon!