Close the two remaining P0 gaps: process termination and store wipe - #65
Close the two remaining P0 gaps: process termination and store wipe#65mo4islona wants to merge 1 commit into
Conversation
39583bc to
8d4fae6
Compare
Four remote-triggerable termination paths, one per subsystem: an address the document got wrong panicked the reconciler, an unparseable roster peer id panicked the reader, the download watchdog multiplied a document-chosen file count into Duration overflow, and a registry read failure refused the start outright. Under ADR-14's supervision tree each one is a whole-process outage, so a scheduler bug or a registry blip is a fleet outage. Separately, reconciliation deleted whatever the assignment stopped naming, so one short document wiped a multi-terabyte store in a single pass. ADR-17 is ratified and implemented: a batch over P-DEL-FLOOR is held whole rather than trimmed to the bar, because trimming still wipes the store, just over several passes. That hold is a bounded delay, not a veto. An indefinite one breaks RS-3 and LIV-4 outright — a withheld chunk meets LIV-4's precondition exactly, and its bytes belong to no term of RS-3's excess bound — and strands disk on a worker the network has legitimately shrunk. What the window buys is not human intervention but self-correction: a glitch is fixed by the next publication while a real shrink keeps being republished, so given P-DEL-HOLD-MAX the two separate themselves. The loop owns the timer, so LIV-4 gets its eviction with no further input event. Since the window is evidence, it belongs to the batch it is evidence about. A different wipe cannot spend it, or a second bad document arriving near expiry would execute with no delay; and a member a pin left behind keeps the authorization its batch earned, or one pinned chunk would cost a second window and double LIV-4's bound. The batch counts every chunk the assignment dropped, pinned or not: a pin lasts one query, and letting it shrink the batch would only deal the wipe out in instalments. Which chunks failed to resolve is reconciliation state, so it lives in `State` beside the sets it belongs with. Held outside as a fourth lock it added an ordering rule that existed only in a comment at one call site, and the alarm read promptly took it the other way round — ADR-10's hierarchy is worth keeping small enough that it cannot be got wrong. The reader panic is contained rather than validated away (ADR-21): structural validation is ADR-18's, which is larger and unratified. Containment costs the whole document where per-item degradation would cost one chunk. Both closures needed an alarm to report through, which is OB-12's first binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@mo4islona I think Instead of having this on the worker the practical approach would be to have something similar on the scheduler, but that's again depends on the shape of the data. If suddenly couple of workers leave and the reshuffling change significantly the picture of chunk distributions it's not realistic to keep worker decide what it should and shouldn't keep. Also |
Closes the last two P0 rows in the spec/13 gap register. Both are remote-triggerable
by whoever publishes an assignment document.
GAP-2 — externally supplied content could terminate the process
Under ADR-14's supervision tree any panic in a loop is a whole-process outage, so a
scheduler bug or a registry blip becomes a fleet outage. Four paths, one per subsystem:
list_filesreturningNonedefers that chunk and alarms; the rest of the document applies (FM-11)DatasetsIndex::newis a panic-catching boundary (ADR-21) → the document is rejected whole (FM-12); thespawn_blockingjoin repeats the catch instead of.expect-ingwatchdog_timeoutsaturates, and a zero-file chunk gets one timeout instead of expiring immediatelyAllocationsCheckerresolves its on-chain id lazily in the polling loop and alarms while unresolved (FM-52)Deferred chunks are held out of the pending set only for the duration of a pass, so the
download loop still terminates, and applying a new index notifies the loop even when the
chunk set is unchanged — otherwise a repaired address would wait for an unrelated event.
GAP-3 — no reconciliation deletion floor
Reconciliation deleted whatever the assignment stopped naming, so one short document
wiped a multi-terabyte store in a single pass. ADR-17 is ratified and implemented: an
application that would evict more than
P-DEL-FLOOR(default 50 %,--deletion-floor)of the stored chunks evicts none of them, raises an alarm, and re-runs the test every
pass, so a restoring assignment releases the hold by itself. Held data stays queryable.
The batch is held whole rather than trimmed to the bar, because trimming still wipes the
store, just over several passes.
The hold is a bounded delay, not a veto. An indefinite one breaks two MUSTs outright:
a withheld chunk meets LIV-4's precondition exactly (in the available set, not in the
assignment, unpinned) and never leaves, and its bytes belong to no term of RS-3's excess
bound. It also strands disk on a worker the network legitimately shrank — and since
eviction precedes fetching in the loop, a worker that cannot free space needs twice the
headroom to converge on a large rebalance. So the batch goes through after
P-DEL-HOLD-MAX(default 1 h,DELETION_HOLD_MAX_SEC), off a timer the reconciliationloop owns, which is what LIV-4's "without requiring any further input event" demands.
What the window buys is machine self-correction, not human response: a scheduler glitch
is corrected by the next publication, while a real shrink keeps being republished for the
whole window, so the two separate themselves. A bug that outlives the window still wipes
— this guards against a glitch, not a sustained fault. Raising the floor to 1 is the
operator override.
The floor counts every chunk the assignment dropped, pinned or not. A pin lasts one
query, so letting it shrink the batch would deal a wipe out in instalments instead of
holding it. Pins keep deciding which permitted removals run now (RS-2 precedence).
Spec consequences recorded rather than hand-waved: RS-3 gains an explicit
Hterm forheld bytes bounded by
P-DEL-HOLD-MAX, LIV-4's bound becomesP-DEL-HOLD-MAX + P-EVICT-BOUNDfor the hold path, and ADR-17 states plainly what the guard does notcover.
Alarms
Both closures needed somewhere to report, which is OB-12's first real binding:
worker_alarms{reason}plus a per-instanceAlarmssnapshot. The conformance testsassert the instance state — the gauges are process-global and would race across tests.
Testing
New
tests/assignment_faults.rs(CT-4 assignment-fault corpus, wired into the CIconformance job) with 7 tests; the unit tier grows to 47. Every fix was verified by
reverting it and confirming its test fails — including the hold's self-wake (the loop
hangs 30 s without the timer arm) and the pin-counting fix — so none of them pass
vacuously. Green on the unit tier, the conformance tier under both CI feature configs,
cargo fmt, the clippy gate, andcheck_spec.py.Judgment calls worth a look
GAP-2and accepted ADRs are append-only, so the id dangles.id-undefinednow exemptsdecisions/for theGAP-prefix, and only for ADRs whose status isAccepted—proposed ones stay checked, which is how ADR-18's now-stale claim about the reader panic
got caught and corrected. The alternative is keeping closed rows with a status column.
id is recorded before application). Every rejection cause is a property of the document
bytes, so re-fetching identical bytes is waste, and acknowledging the id only on success
would loop forever on a genuinely bad document — GAP-9's head-of-line blocking. Recovery
is bounded by the next published id. Say the word if you want it tracked as a gap row.
P-GATE-PR-TIMEtimeout. The tests run in under a second; the cost is one more ~300 MB link per matrix
config. The timeout is unchanged — worth watching on the first run.
🤖 Generated with Claude Code