Skip to main content

planarian_txn/
kernel.rs

1//! Driving one transaction through the journal (§12 step 7, step 8, the
2//! crash-recovery table, ownership and fencing).
3//!
4//! The kernel owns ordering: what is journaled before which backend call, who
5//! may advance, and what recovery does from each state. Proposal handling,
6//! policy, staging and approval sit above it in `exec`; the backends, probes
7//! and process control sit below it, behind `Host`.
8
9use crate::journal::{Journal, JournalError};
10use crate::ledger::{LedgerError, LiveTxn, OpRecord, Reconciliation, Record};
11use crate::machine::tag;
12use crate::store::Store;
13use planarian_schema::{
14    Actor, Attribution, GenerationBackend, GenerationId, Hash, InventoryBackend, JobId, Lease, Millis, OpId, Outcome,
15    Override, OverrideDesired, OwnerId, RecoveryStrategy, RejectReason, Timestamp, Transaction, Transition, TxnId,
16    TxnState, UnitId,
17};
18
19/// Job-drain wait during takeover (§16 operational limits).
20pub const JOB_DRAIN: Millis = Millis::secs(60);
21/// How long a lease may go unrenewed before the watchdog treats the owner as gone,
22/// once the transaction's deadline has passed.
23pub const LEASE_GRACE: Millis = Millis::secs(30);
24
25/// One row of the crash-recovery table (§12): what recovery does from the
26/// state a transaction was left in. `Kernel::recover_steps` dispatches on it.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum RecoveryAction {
29    /// Nothing was mutated: end `Rejected(Interrupted)`.
30    Reject,
31    /// A runtime operation: re-observe and commit.
32    Reobserve,
33    /// A generation was being activated: Checking if the candidate is active, else revert.
34    CandidateActive,
35    /// A generation was being checked: probes decide.
36    CheckGeneration,
37    /// The boot entry was written: probes, then publish again and promote, or revert.
38    RecheckPublished,
39    /// The override was promoted: write Committed.
40    WriteCommitted,
41    /// A generation revert was under way: run it again (it is idempotent).
42    RevertGeneration,
43    /// A boot-only publication was under way: ask what was armed (R01).
44    Publishing,
45    /// A driver was being bound: Checking if it is bound, else revert.
46    DriverBound,
47    /// A driver bind was being checked: probes decide.
48    CheckDevice,
49    /// A driver revert was under way: run it again.
50    RevertDevice,
51    /// A boot-only candidate waits for its boot (§12 step 7): leave it until
52    /// a reboot; then confirm it if it booted, else Reverted.
53    AwaitBoot,
54    /// No recovery path: RecoveryFailed.
55    Fail,
56}
57
58/// Whether the watchdog should take a transaction over (§12 recovery
59/// watchdog): only in the window where mutation may be under way, never from
60/// itself, and only when the owner is dead or has let both the transaction's
61/// deadline and its lease's grace period pass.
62pub fn should_take_over(
63    state: &TxnState,
64    boot_only: bool,
65    mine: bool,
66    owner_alive: bool,
67    now: Timestamp,
68    deadline: Timestamp,
69    renewed: Timestamp,
70) -> bool {
71    // A boot-only Promoted waits for the daemon's confirmation checkpoint
72    // (§12 crash table); the watchdog does not advance it.
73    let in_window = match state {
74        TxnState::Activating
75        | TxnState::Checking
76        | TxnState::Publishing
77        | TxnState::Published
78        | TxnState::Reverting => true,
79        TxnState::Promoted => !boot_only,
80        _ => false,
81    };
82    if !in_window || mine {
83        return false;
84    }
85    let unrenewed = now > deadline && now > renewed.plus(LEASE_GRACE);
86    !owner_alive || unrenewed
87}
88
89/// The epoch a takeover writes: above both the journaled lease's and the
90/// store's fence (which may be ahead if an earlier takeover crashed between
91/// the two writes). `None` when epochs are exhausted.
92pub fn next_epoch(lease_epoch: u64, fence: Option<u64>) -> Option<u64> {
93    lease_epoch.max(fence.unwrap_or(0)).checked_add(1)
94}
95
96/// Whether a backend call tagged with `epoch` may proceed, given the store's
97/// fence for its transaction (I15): only the current owner's epoch passes.
98pub fn fence_admits(fence: Option<u64>, epoch: u64) -> bool {
99    fence == Some(epoch)
100}
101
102/// The hash-compared override replacement (§9 step 4).
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum SwapStep {
105    /// The live file already holds the target.
106    Done,
107    /// The live file holds the expected source: replace it.
108    Swap,
109    /// Someone else changed it: never overwrite.
110    ThirdParty,
111}
112
113pub fn swap_step(current: &Hash, from: &Hash, to: &Hash) -> SwapStep {
114    if current == to {
115        SwapStep::Done
116    } else if current == from {
117        SwapStep::Swap
118    } else {
119        SwapStep::ThirdParty
120    }
121}
122
123/// The crash-recovery table (§12).
124pub fn recovery_action(state: &TxnState, strategy: RecoveryStrategy) -> RecoveryAction {
125    use RecoveryAction as A;
126    use RecoveryStrategy as S;
127    match (state, strategy) {
128        (s, _) if s.is_pre_mutation() => A::Reject,
129        (TxnState::Activating | TxnState::Checking, S::ReobserveOnly) => A::Reobserve,
130        (TxnState::Activating, S::RevertGeneration) => A::CandidateActive,
131        (TxnState::Checking, S::RevertGeneration) => A::CheckGeneration,
132        (TxnState::Published, S::RevertGeneration) => A::RecheckPublished,
133        (TxnState::Promoted, S::RevertGeneration) => A::WriteCommitted,
134        (TxnState::Reverting, S::RevertGeneration) => A::RevertGeneration,
135        (TxnState::AwaitingBoot, S::RevertGeneration) => A::AwaitBoot,
136        (TxnState::Publishing, S::RevertGeneration) => A::Publishing,
137        (TxnState::Activating, S::UnbindDevice) => A::DriverBound,
138        (TxnState::Checking, S::UnbindDevice) => A::CheckDevice,
139        (TxnState::Reverting, S::UnbindDevice) => A::RevertDevice,
140        _ => A::Fail,
141    }
142}
143
144/// Which probe run a check belongs to.
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub enum CheckPhase {
147    /// After activating the candidate (or binding the new driver).
148    Candidate,
149    /// After reverting.
150    Revert,
151}
152
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum Verdict {
155    Pass,
156    Fail(String),
157}
158
159/// Everything the kernel needs from below. One generic parameter (no `dyn`, §6).
160pub trait Host {
161    type Gen: GenerationBackend;
162    type Inv: InventoryBackend;
163
164    fn generations(&self) -> &Self::Gen;
165    fn inventory(&self) -> &Self::Inv;
166    /// Perform a `ReobserveOnly` transition's backend effect (restart, stop,
167    /// runtime option, GC). Override bookkeeping is the kernel's job, not this.
168    fn execute_runtime(&self, t: &Transition, op: &OpId) -> Outcome;
169    /// Settle, then observe and compare against the expectation; the outcome is
170    /// recorded, never acted on (§12 step 7, UnitOp).
171    fn reobserve(&self, txn: &Transaction) -> Outcome;
172    /// Run the applicable Readiness probes from the candidate's UnitDefs and the
173    /// installed core, plus the expectation and executor-owned requirements.
174    fn check(&self, txn: &Transaction, phase: CheckPhase) -> Verdict;
175    /// Remove a GC root (I3). Returns false if it could not be removed.
176    fn release_gc_root(&self, path: &str) -> bool;
177    fn now(&self) -> Timestamp;
178    /// The boot clock (`CLOCK_BOOTTIME`): leases, deadlines and waits (§12h).
179    fn boot_now(&self) -> Timestamp;
180    fn sleep(&self, d: Millis);
181    /// SIGTERM then SIGKILL the process group via pidfd; true once confirmed dead.
182    fn kill_worker(&self, pid: u32) -> bool;
183    fn pid_alive(&self, pid: u32) -> bool;
184    /// §12e, I25: the security fixes `txn`'s candidate brings, absent from
185    /// the floor generation and every generation between it and the
186    /// candidate (every earlier one, with no floor). Part of the commit:
187    /// an error here means the transaction does not commit yet.
188    fn floor_fixes(&self, _txn: &Transaction, _floor: Option<GenerationId>) -> Result<Vec<String>, Outcome> {
189        Ok(vec![])
190    }
191}
192
193#[derive(Debug)]
194pub enum KernelError {
195    Journal(JournalError),
196    /// A fault-injected crash (fake harness): this handle must be discarded.
197    Crashed,
198    /// Another owner holds the lease; this handle no longer owns the transaction.
199    LostLease(TxnId),
200    NotLive(TxnId),
201}
202
203impl From<JournalError> for KernelError {
204    fn from(e: JournalError) -> Self {
205        match e {
206            JournalError::Ledger(LedgerError::StaleEpoch { txn, .. }) => KernelError::LostLease(txn),
207            e => KernelError::Journal(e),
208        }
209    }
210}
211
212type R<T> = Result<T, KernelError>;
213
214/// Fault injection for the fake harness: stop dead (as if SIGKILLed) right after
215/// a record is durable.
216#[derive(Clone, Debug, PartialEq, Eq)]
217pub enum CrashPoint {
218    /// After the Advance record into this state (by `machine::tag`).
219    State(&'static str),
220    /// After the OpBegin record for this call, before the call is made.
221    OpBegun(&'static str),
222    /// After the call returned, before its OpAck is journaled.
223    OpCalled(&'static str),
224}
225
226pub struct Kernel<S: Store, H: Host> {
227    pub journal: Journal<S>,
228    pub host: H,
229    pub me: OwnerId,
230    pub crash_at: Option<CrashPoint>,
231    /// The worker pid recorded in leases this handle takes.
232    pub worker_pid: Option<u32>,
233    /// Epochs this handle holds, by transaction. Every record and backend call
234    /// carries the handle's own epoch, never the ledger's current one, so a
235    /// handle whose lease was taken is refused (I15).
236    held: std::collections::BTreeMap<TxnId, u64>,
237}
238
239impl<S: Store, H: Host> Kernel<S, H> {
240    pub fn new(journal: Journal<S>, host: H, me: OwnerId) -> Self {
241        Kernel { journal, host, me, crash_at: None, worker_pid: None, held: Default::default() }
242    }
243
244    fn live(&mut self, id: TxnId) -> R<LiveTxn> {
245        self.journal.sync_ledger()?.live.get(&id).cloned().ok_or(KernelError::NotLive(id))
246    }
247
248    fn epoch(&self, txn: TxnId) -> R<u64> {
249        self.held.get(&txn).copied().ok_or(KernelError::LostLease(txn))
250    }
251
252    fn record(&mut self, r: Record) -> R<()> {
253        let now = self.host.now();
254        let txn = match &r {
255            Record::Advance { txn, .. } | Record::Takeover { txn, .. } | Record::Renew { txn, .. } => Some(*txn),
256            Record::OpBegin { op, .. } | Record::OpAck { op, .. } => Some(op.txn),
257            _ => None,
258        };
259        match self.journal.append(now, r) {
260            Err(JournalError::Full) => {
261                // A transaction that may already have mutated the system cannot
262                // be left without a record: fixed-size sentinel, interlock (§12c).
263                let mutating =
264                    txn.and_then(|t| self.journal.ledger().live.get(&t)).filter(|l| !l.txn.state.is_pre_mutation());
265                if let Some(l) = mutating {
266                    let id = l.txn.id;
267                    self.journal.write_sentinel(id, "journal full on the recovery path")?;
268                }
269                Err(KernelError::Journal(JournalError::Full))
270            }
271            r => r.map(|_| ()).map_err(KernelError::from),
272        }
273    }
274
275    fn advance(&mut self, txn: &Transaction, to: TxnState, note: &str) -> R<()> {
276        self.advance_for(txn, to, note, None)
277    }
278
279    fn advance_for(&mut self, txn: &Transaction, to: TxnState, note: &str, withdrawn_for: Option<TxnId>) -> R<()> {
280        let epoch = self.epoch(txn.id)?;
281        let rec = Record::Advance { txn: txn.id, epoch, to: to.clone(), note: note.into(), withdrawn_for };
282        self.record(rec)?;
283        if to.is_terminal()
284            && to != TxnState::Committed
285            && let Some(root) = &txn.gc_root
286        {
287            self.host.release_gc_root(root);
288        }
289        if self.crash_at == Some(CrashPoint::State(tag(&to))) {
290            return Err(KernelError::Crashed);
291        }
292        Ok(())
293    }
294
295    /// Journal a durable OpId, then make the mutating call tagged with it, then
296    /// journal its outcome.
297    fn call(&mut self, txn: TxnId, name: &'static str, f: impl FnOnce(&H, &OpId) -> Outcome) -> R<Outcome> {
298        self.call_op(txn, name, f).map(|(_, o)| o)
299    }
300
301    fn call_op(&mut self, txn: TxnId, name: &'static str, f: impl FnOnce(&H, &OpId) -> Outcome) -> R<(OpId, Outcome)> {
302        let epoch = self.epoch(txn)?;
303        let l = self.journal.sync_ledger()?.live.get(&txn).cloned().ok_or(KernelError::NotLive(txn))?;
304        let op = OpId { txn, epoch, seq: l.ops.len() as u64 + 1 };
305        self.record(Record::OpBegin { op, call: name.into() })?;
306        if self.crash_at == Some(CrashPoint::OpBegun(name)) {
307            return Err(KernelError::Crashed);
308        }
309        let outcome = f(&self.host, &op);
310        if outcome.is_fenced() {
311            return Err(KernelError::LostLease(txn));
312        }
313        if self.crash_at == Some(CrashPoint::OpCalled(name)) {
314            return Err(KernelError::Crashed);
315        }
316        self.record(Record::OpAck { op, outcome: outcome.clone() })?;
317        Ok((op, outcome))
318    }
319
320    /// After a call that did not report success, make sure the work it started
321    /// has stopped before deciding anything from the observed state: wait for
322    /// its tagged jobs, cancel what remains, and wait again.
323    fn drain_op(&mut self, txn: TxnId, op: &OpId) -> R<Result<(), String>> {
324        let jobs = match self.host.generations().jobs_tagged(op) {
325            Ok(Some(jobs)) => jobs,
326            Ok(None) => return Ok(Err(format!("backend cannot say what op {op} started"))),
327            Err(o) => return Ok(Err(format!("jobs_tagged({op}): {o:?}"))),
328        };
329        if jobs.is_empty() || self.wait_gone(&jobs)? {
330            return Ok(Ok(()));
331        }
332        for job in &jobs {
333            self.call(txn, "cancel", |h, op| h.generations().cancel(job, op))?;
334        }
335        if self.wait_gone(&jobs)? {
336            Ok(Ok(()))
337        } else {
338            Ok(Err(format!("jobs of op {op} still pending after cancel and {}s drain", JOB_DRAIN.0 / 1000)))
339        }
340    }
341
342    /// Wait up to `JOB_DRAIN` for `jobs` to leave the supervisor's queue.
343    fn wait_gone(&mut self, jobs: &[JobId]) -> R<bool> {
344        let deadline = self.host.boot_now().plus(JOB_DRAIN);
345        loop {
346            match self.host.generations().pending_jobs() {
347                Ok(pending) if !jobs.iter().any(|j| pending.contains(j)) => return Ok(true),
348                Ok(_) => {}
349                Err(_) => return Ok(false),
350            }
351            if self.host.boot_now() >= deadline {
352                return Ok(false);
353            }
354            self.host.sleep(Millis::secs(1));
355        }
356    }
357
358    // ---------- Creation and pre-mutation exits ----------
359
360    /// Journal `Prepared` (§12 step 4) with this handle as owner at epoch 1.
361    /// The transaction's id is assigned here.
362    pub fn prepare(&mut self, mut txn: Transaction) -> R<TxnId> {
363        // Keep the preallocation's headroom for recovery records (§12c).
364        if self.journal.needs_compaction() {
365            self.journal.compact()?;
366        }
367        let now = self.host.now();
368        let me = self.me.clone();
369        let worker = self.worker_pid;
370        self.journal
371            .locked(|j| {
372                let id = TxnId(j.ledger().next_txn.max(1));
373                txn.id = id;
374                txn.state = TxnState::Prepared;
375                txn.recovery = txn.transition.recovery();
376                txn.lease = Lease {
377                    txn: id,
378                    owner: me,
379                    epoch: 1,
380                    worker_pid: worker,
381                    renewed: self.host.boot_now(),
382                    jobs: vec![],
383                };
384                // Exclusive: an id is never prepared twice (C5).
385                j.store().create_fence(id, 1)?;
386                let r = j.append(now, Record::Prepared(Box::new(txn))).map(|_| id);
387                if r.is_err() {
388                    // No record names it; take the fence back.
389                    let _ = j.store().forget_fences(|t| t != id);
390                }
391                r
392            })?
393            .map_err(KernelError::from)
394            .inspect(|id| {
395                self.held.insert(*id, 1);
396            })
397    }
398
399    /// Terminal `Rejected` from a pre-mutation state (§12 step 5).
400    pub fn reject(&mut self, id: TxnId, reason: RejectReason) -> R<TxnState> {
401        let l = self.live(id)?;
402        let to = TxnState::Rejected(reason);
403        self.advance(&l.txn, to.clone(), "rejected before mutation")?;
404        Ok(to)
405    }
406
407    /// §12f: withdraw `id`, waiting in `Built`, for the rollback `rollback`
408    /// (`Rejected(Interrupted)`, naming it in the same record).
409    pub fn withdraw(&mut self, id: TxnId, rollback: TxnId) -> R<TxnState> {
410        let l = self.live(id)?;
411        let to = TxnState::Rejected(RejectReason::Interrupted);
412        self.advance_for(&l.txn, to.clone(), "withdrawn for a rollback", Some(rollback))?;
413        Ok(to)
414    }
415
416    pub fn cancel(&mut self, id: TxnId, by: Actor) -> R<TxnState> {
417        let l = self.live(id)?;
418        let to = TxnState::Cancelled(by);
419        self.advance(&l.txn, to.clone(), "withdrawn before mutation")?;
420        Ok(to)
421    }
422
423    // ---------- Activation scheduling (§12f) ----------
424
425    /// A verified generation candidate waits in `Built` for an activation
426    /// window. It holds no lease-gated mutation and counts as active for I6;
427    /// `run` takes it on from there.
428    pub fn wait_built(&mut self, id: TxnId, why: &str) -> R<TxnState> {
429        let l = self.live(id)?;
430        if l.txn.state != TxnState::Built {
431            self.advance(&l.txn, TxnState::Built, why)?;
432        }
433        Ok(TxnState::Built)
434    }
435
436    /// The boot-only path (§12 step 7): publish the candidate as a one-shot
437    /// next-boot entry, the base still the default, and wait in
438    /// `AwaitingBoot`; the boot it was published for confirms or reverts it
439    /// (`await_boot`).
440    pub fn run_boot_only(&mut self, id: TxnId) -> R<TxnState> {
441        let l = self.live(id)?;
442        if self.held.get(&id) != Some(&l.txn.lease.epoch) {
443            return Err(KernelError::LostLease(id));
444        }
445        let txn = l.txn;
446        let Some((cand, path)) = txn.candidate.clone() else {
447            return self.reject(id, RejectReason::Other("no candidate".into()));
448        };
449        let expected = txn.candidate_nar;
450        if self.host.generations().verify_candidate(&path, expected.as_ref()) != Ok(true) {
451            return self.reject(id, RejectReason::Other("candidate failed verification".into()));
452        }
453        self.wait_built(id, "takes effect at boot")?;
454        // R01: durable before the first boot mutation, so a crash in the
455        // publication is found and asked about, never missed.
456        self.advance(&txn, TxnState::Publishing, "publishing to boot once")?;
457        // S01: the running generation, which publishes the candidate, stays
458        // the default and is the fallback; never known_good automatically.
459        let base = txn.base;
460        let o = self.call(id, "publish_boot_once", |h, op| h.generations().publish_boot_once(cand, base, op))?;
461        if !o.is_success() {
462            // All or nothing (the backend's contract): nothing was published.
463            let to = TxnState::Rejected(RejectReason::Other(format!("publish_boot_once: {o:?}")));
464            self.advance(&txn, to.clone(), "nothing was published")?;
465            return Ok(to);
466        }
467        self.advance(&txn, TxnState::AwaitingBoot, "published to boot once; restart to finish")?;
468        Ok(TxnState::AwaitingBoot)
469    }
470
471    /// The crash table's Publishing row (§12): the `publish_boot_once` op
472    /// acknowledged success → AwaitingBoot; no acknowledgement → ask the boot
473    /// loader whether the one-shot entry is armed: armed → AwaitingBoot, not
474    /// armed → Rejected(Interrupted); it cannot say → RecoveryFailed.
475    fn recover_publishing(&mut self, txn: &Transaction, ops: &[OpRecord]) -> R<TxnState> {
476        let Some(cand) = Self::candidate(txn) else {
477            return self.recovery_failed(txn, "Publishing without a candidate");
478        };
479        let publish = ops.iter().rev().find(|o| o.call == "publish_boot_once");
480        // Only a definite answer settles it: success armed the entry, and a
481        // failure armed nothing (all or nothing). No answer, an Unknown (as a
482        // takeover acknowledges the calls it superseded) or a Partial: ask.
483        let armed = match publish.and_then(|o| o.ack.as_ref()) {
484            Some(Outcome::Succeeded) => true,
485            Some(Outcome::Failed(_)) => false,
486            _ => match self.host.generations().one_shot_pending() {
487                Ok(pending) => pending == Some(cand),
488                Err(o) => {
489                    return self
490                        .recovery_failed(txn, &format!("cannot tell whether the one-shot entry is armed: {o:?}"));
491                }
492            },
493        };
494        if armed {
495            self.advance(txn, TxnState::AwaitingBoot, "recovery: the one-shot entry is armed")?;
496            Ok(TxnState::AwaitingBoot)
497        } else {
498            let to = TxnState::Rejected(RejectReason::Interrupted);
499            self.advance(txn, to.clone(), "recovery: nothing was published")?;
500            Ok(to)
501        }
502    }
503
504    // ---------- Fresh execution ----------
505
506    /// Execute a prepared (and, if AskFirst, approved) transaction to a terminal state.
507    pub fn run(&mut self, id: TxnId) -> R<TxnState> {
508        let l = self.live(id)?;
509        if self.held.get(&id) != Some(&l.txn.lease.epoch) {
510            return Err(KernelError::LostLease(id));
511        }
512        if !l.txn.state.is_pre_mutation() {
513            return self.recover_steps(id);
514        }
515        match l.txn.recovery {
516            RecoveryStrategy::ReobserveOnly => self.run_reobserve(&l.txn),
517            RecoveryStrategy::RevertGeneration => self.run_generation(&l.txn),
518            RecoveryStrategy::UnbindDevice => self.run_device(&l.txn),
519            RecoveryStrategy::RestoreSnapshotThenRevert => {
520                // Phase C. Refuse rather than run half a protocol.
521                self.reject(id, RejectReason::Other("RestoreSnapshot is Phase C".into()))
522            }
523        }
524    }
525
526    fn run_reobserve(&mut self, txn: &Transaction) -> R<TxnState> {
527        let caller = txn.facts.caller.clone();
528        // The suppression is journaled before the stop (§12, §14), and before
529        // Activating: once the user's "stop" is recorded at all, the repair
530        // loop leaves the unit alone, whatever happens to the rest.
531        // An override the daemon or an agent sets would be gamed success (§15);
532        // a caller that is not a user never gets this far through policy.
533        match &txn.transition {
534            Transition::StopAndSuppress { unit, until, reason } => {
535                let s = Override::attested(
536                    unit.clone(),
537                    OverrideDesired::Stopped,
538                    until.clone(),
539                    reason.clone(),
540                    caller.clone(),
541                );
542                self.record(Record::OverrideSet(s))?;
543            }
544            Transition::SetOverride(s) => match s.desired {
545                OverrideDesired::LeaveAlone | OverrideDesired::Stopped => {
546                    let s = Override { by: caller.clone(), attribution: Attribution::Attested, ..s.clone() };
547                    self.record(Record::OverrideSet(s))?;
548                }
549                OverrideDesired::Absent | OverrideDesired::Detected => {
550                    return self
551                        .reject(txn.id, RejectReason::Other("Phase A overrides are LeaveAlone or Stopped".into()));
552                }
553            },
554            _ => {}
555        }
556        self.advance(txn, TxnState::Activating, "")?;
557        let outcome = match &txn.transition {
558            Transition::StopAndSuppress { unit, .. } => {
559                let stop = Transition::StopUnit(unit.clone());
560                self.call(txn.id, "stop", |h, op| h.execute_runtime(&stop, op))?
561            }
562            // Journaled above; a Stopped override also stops the unit.
563            Transition::SetOverride(s) if s.desired == OverrideDesired::Stopped => {
564                let stop = Transition::StopUnit(s.unit.clone());
565                self.call(txn.id, "stop", |h, op| h.execute_runtime(&stop, op))?
566            }
567            Transition::SetOverride(_) => Outcome::Succeeded,
568            Transition::LiftOverride(u) => {
569                self.record(Record::OverrideLifted(u.clone()))?;
570                Outcome::Succeeded
571            }
572            Transition::Resolve { txn: target, resolution } => {
573                self.record(Record::Resolved { txn: *target, resolution: *resolution, by: caller })?;
574                Outcome::Succeeded
575            }
576            t => {
577                let t = t.clone();
578                self.call(txn.id, "runtime", |h, op| h.execute_runtime(&t, op))?
579            }
580        };
581        self.advance(txn, TxnState::Checking, &format!("{outcome:?}"))?;
582        self.finish_reobserve(txn)
583    }
584
585    fn finish_reobserve(&mut self, txn: &Transaction) -> R<TxnState> {
586        let observed = self.host.reobserve(txn);
587        self.advance(txn, TxnState::Committed, &format!("observed: {observed:?}"))?;
588        Ok(TxnState::Committed)
589    }
590
591    fn candidate(txn: &Transaction) -> Option<GenerationId> {
592        txn.candidate.as_ref().map(|(g, _)| *g)
593    }
594
595    fn run_generation(&mut self, txn: &Transaction) -> R<TxnState> {
596        let Some((cand, path)) = txn.candidate.clone() else {
597            return self.reject(txn.id, RejectReason::Other("no candidate".into()));
598        };
599        let expected = txn.candidate_nar;
600        match self.host.generations().verify_candidate(&path, expected.as_ref()) {
601            Ok(true) => {}
602            _ => return self.reject(txn.id, RejectReason::Other("candidate failed verification".into())),
603        }
604        self.advance(txn, TxnState::Activating, "")?;
605        let (op, outcome) = self.call_op(txn.id, "activate_live", |h, op| h.generations().activate_live(cand, op))?;
606        if !outcome.is_success()
607            && let Err(why) = self.drain_op(txn.id, &op)?
608        {
609            return self.recovery_failed(txn, &why);
610        }
611        if outcome.is_success() || self.host.generations().current().ok() == Some(cand) {
612            self.advance(txn, TxnState::Checking, &format!("{outcome:?}"))?;
613            self.check_then_commit(txn)
614        } else {
615            self.revert_generation(txn, &format!("activation: {outcome:?}"))
616        }
617    }
618
619    /// Activating a generation restarts every active target, and with them
620    /// the services they want, including ones a user stopped on purpose.
621    /// Stop those again (principle 9: don't fight the user). Idempotent.
622    fn restop_suppressed(&mut self, txn: &Transaction) -> R<()> {
623        let now = self.host.now();
624        let units: Vec<UnitId> = self
625            .journal
626            .sync_ledger()?
627            .overrides
628            .iter()
629            .filter(|s| s.desired == OverrideDesired::Stopped && s.holds_at(now))
630            .map(|s| s.unit.clone())
631            .collect();
632        for u in units {
633            let stop = Transition::StopUnit(u);
634            self.call(txn.id, "stop(suppressed)", |h, op| h.execute_runtime(&stop, op))?;
635        }
636        Ok(())
637    }
638
639    /// From Checking: probes decide.
640    fn check_then_commit(&mut self, txn: &Transaction) -> R<TxnState> {
641        self.restop_suppressed(txn)?;
642        match self.host.check(txn, CheckPhase::Candidate) {
643            Verdict::Pass => self.commit_from_checking(txn),
644            Verdict::Fail(why) => self.revert_generation(txn, &why),
645        }
646    }
647
648    /// publish_boot → Published → promote → Promoted → Committed. Each step is idempotent.
649    /// Only after this owner's own probes passed (check_then_commit).
650    fn commit_from_checking(&mut self, txn: &Transaction) -> R<TxnState> {
651        let cand = Self::candidate(txn).expect("generation transactions have a candidate");
652        let published = self.call(txn.id, "publish_boot", |h, op| h.generations().publish_boot(cand, op))?;
653        if !published.is_success() {
654            // /boot full and the like: a Checking failure (§12c).
655            return self.revert_generation(txn, &format!("publish_boot: {published:?}"));
656        }
657        self.advance(txn, TxnState::Published, "probes passed")?;
658        self.promote_and_commit(txn)
659    }
660
661    fn promote_and_commit(&mut self, txn: &Transaction) -> R<TxnState> {
662        match self.swap_override(txn, txn.base_override, txn.candidate_override)? {
663            Swap::Done => {}
664            Swap::Failed(why) => return self.revert_generation(txn, &format!("promotion: {why}")),
665            Swap::ThirdParty => {
666                self.advance(txn, TxnState::RecoveryFailed, "override changed by a third party during promotion")?;
667                return Ok(TxnState::RecoveryFailed);
668            }
669        }
670        self.advance(txn, TxnState::Promoted, "")?;
671        self.write_committed(txn)
672    }
673
674    /// The transaction took the boot-only path (§12 step 7): its journal
675    /// path passed AwaitingBoot.
676    pub fn boot_only(&self, id: TxnId) -> bool {
677        self.journal.ledger().live.get(&id).is_some_and(|l| l.boot_only || l.path.iter().any(|p| p == "AwaitingBoot"))
678    }
679
680    /// §10 admission of a boot-only candidate that booted and passed its
681    /// Readiness probes: it becomes the default and its override is
682    /// promoted; the transaction is Promoted (admitted, not confirmed) until
683    /// the 10-minute confirmation checkpoint of this boot.
684    fn admit_boot_only(&mut self, txn: &Transaction, cand: GenerationId) -> R<TxnState> {
685        let o = self.call(txn.id, "publish_boot", |h, op| h.generations().publish_boot(cand, op))?;
686        if !o.is_success() {
687            return self.withdraw_boot_only(txn, false, &format!("publish_boot: {o:?}"));
688        }
689        match self.swap_override(txn, txn.base_override, txn.candidate_override)? {
690            Swap::Done => {}
691            Swap::Failed(why) => return self.withdraw_boot_only(txn, false, &format!("promotion: {why}")),
692            Swap::ThirdParty => {
693                return self.recovery_failed(txn, "override changed by a third party during promotion");
694            }
695        }
696        self.advance(txn, TxnState::Promoted, "admitted at boot; confirmed at the 10-minute checkpoint")?;
697        Ok(TxnState::Promoted)
698    }
699
700    /// §10 confirmation, at the 10-minute checkpoint of the boot that
701    /// admitted a boot-only candidate (only the daemon calls this): the
702    /// candidate still runs and the canonical probes pass → bookkeeping and
703    /// Committed; otherwise it is withdrawn (§12 step 7).
704    pub fn confirm_boot_only(&mut self, id: TxnId, healthy: bool) -> R<TxnState> {
705        let l = self.live(id)?;
706        if self.held.get(&id) != Some(&l.txn.lease.epoch) {
707            return Err(KernelError::LostLease(id));
708        }
709        let txn = l.txn;
710        if txn.state != TxnState::Promoted || !self.boot_only(id) {
711            return Ok(txn.state);
712        }
713        let running = self.host.generations().last_booted().ok() == Self::candidate(&txn);
714        if healthy && running {
715            self.write_committed(&txn)
716        } else {
717            self.withdraw_boot_only(&txn, true, "not confirmed: canonical probes failed at the 10-minute checkpoint")
718        }
719    }
720
721    /// Withdraw an admitted or published boot-only candidate (§12 step 7):
722    /// the publishing generation is the boot default again and the base
723    /// override is restored; nothing is activated live, so the machine runs
724    /// the candidate until the next restart, which completes it.
725    fn withdraw_boot_only(&mut self, txn: &Transaction, promoted: bool, why: &str) -> R<TxnState> {
726        if self.live(txn.id)?.txn.state != TxnState::Reverting {
727            self.advance(txn, TxnState::Reverting, why)?;
728        }
729        let back = txn.base;
730        let o = self.call(txn.id, "publish_boot(publishing)", |h, op| h.generations().publish_boot(back, op))?;
731        if !o.is_success() {
732            return self.recovery_failed(txn, &format!("withdraw publish_boot: {o:?}"));
733        }
734        if promoted {
735            match self.swap_override(txn, txn.candidate_override, txn.base_override)? {
736                Swap::Done => {}
737                Swap::Failed(why) => return self.recovery_failed(txn, &format!("override restore: {why}")),
738                Swap::ThirdParty => return self.recovery_failed(txn, "override changed by a third party"),
739            }
740        }
741        self.advance(txn, TxnState::Reverted, why)?;
742        Ok(TxnState::Reverted)
743    }
744
745    /// A boot-only Promoted found at startup (§12 crash table). The same
746    /// boot (a daemon restart): leave it for this boot's checkpoint. A later
747    /// boot: the AwaitingBoot row, admission again: the candidate booted and
748    /// passes → still Promoted, confirmed at this boot's checkpoint;
749    /// otherwise withdrawn.
750    fn readmit_boot_only(&mut self, txn: &Transaction, same_boot: bool) -> R<TxnState> {
751        if same_boot {
752            return Ok(TxnState::Promoted);
753        }
754        let cand = Self::candidate(txn);
755        if self.host.generations().last_booted().ok() != cand {
756            return self.withdraw_boot_only(txn, true, "another generation booted before the change was confirmed");
757        }
758        match self.host.check(txn, CheckPhase::Candidate) {
759            Verdict::Pass => Ok(TxnState::Promoted),
760            Verdict::Fail(why) => self.withdraw_boot_only(txn, true, &why),
761        }
762    }
763
764    /// The crash table's AwaitingBoot row (§12): no reboot since the one-shot
765    /// entry was published (it is still pending), leave it; after a reboot,
766    /// the candidate booted →
767    /// the boot-confirmation probes decide (pass: it becomes the default,
768    /// Promoted, Committed; fail: revert); it did not boot → Reverted.
769    pub fn await_boot(&mut self, txn: &Transaction) -> R<TxnState> {
770        let Some(cand) = Self::candidate(txn) else {
771            return self.recovery_failed(txn, "AwaitingBoot without a candidate");
772        };
773        let gens = self.host.generations();
774        // Still pending, or the boot loader cannot say: no reboot yet; leave it.
775        match gens.one_shot_pending() {
776            Ok(Some(g)) if g == cand => return Ok(TxnState::AwaitingBoot),
777            Err(_) => return Ok(TxnState::AwaitingBoot),
778            _ => {}
779        }
780        if gens.last_booted().ok() != Some(cand) {
781            self.advance(txn, TxnState::Reverted, "the boot-only candidate did not boot; the default entry did")?;
782            return Ok(TxnState::Reverted);
783        }
784        // §10 admission: Readiness probes once; pass → Promoted (admitted);
785        // fail → withdrawn, so the next boot falls back.
786        match self.host.check(txn, CheckPhase::Candidate) {
787            Verdict::Pass => self.admit_boot_only(txn, cand),
788            Verdict::Fail(why) => self.withdraw_boot_only(txn, false, &why),
789        }
790    }
791
792    /// Post-commit bookkeeping, then `Committed` (I25), on every path that
793    /// commits a generation: the daemon's, the watchdog's, the boot-only
794    /// confirmation's. The floor record precedes `Committed`; if the floor
795    /// cannot be derived or journaled, the transaction stays where it is
796    /// (`Promoted`) and is completed later, never committed without it.
797    fn write_committed(&mut self, txn: &Transaction) -> R<TxnState> {
798        if let Some(cand) = Self::candidate(txn) {
799            let floor = self.journal.ledger().security_floor;
800            if floor.is_none_or(|f| f.0 < cand.0) {
801                match self.host.floor_fixes(txn, floor) {
802                    Ok(fixes) if !fixes.is_empty() => self.record(Record::SecurityFloor(Some(cand)))?,
803                    Ok(_) => {}
804                    Err(_) => return Ok(self.live(txn.id)?.txn.state),
805                }
806            }
807            if let Some(h) = &txn.candidate_override {
808                let _ = self.host.generations().record_override(cand, h);
809            }
810        }
811        // A boot-only candidate is never live_tested (§10); it is confirmed.
812        let boot_only = self.boot_only(txn.id);
813        self.advance(txn, TxnState::Committed, "")?;
814        if let Some(cand) = Self::candidate(txn)
815            && !boot_only
816        {
817            self.record(Record::LiveTested(Some(cand)))?;
818        }
819        Ok(TxnState::Committed)
820    }
821
822    /// Hash-compared, idempotent override replacement (§9 step 4): if the live
823    /// file already hashes to `to`, done; if it hashes to `from`, swap; anything
824    /// else means a third party changed it.
825    fn swap_override(&mut self, txn: &Transaction, from: Option<Hash>, to: Option<Hash>) -> R<Swap> {
826        let (Some(from), Some(to)) = (from, to) else {
827            return Ok(Swap::Done);
828        };
829        let current = match self.host.generations().override_hash() {
830            Ok(h) => h,
831            Err(o) => return Ok(Swap::Failed(format!("{o:?}"))),
832        };
833        match swap_step(&current, &from, &to) {
834            SwapStep::Done => return Ok(Swap::Done),
835            SwapStep::ThirdParty => return Ok(Swap::ThirdParty),
836            SwapStep::Swap => {}
837        }
838        let o = self.call(txn.id, "swap_override", |h, op| h.generations().swap_override(&from, &to, op))?;
839        Ok(if o.is_success() { Swap::Done } else { Swap::Failed(format!("{o:?}")) })
840    }
841
842    fn revert_generation(&mut self, txn: &Transaction, why: &str) -> R<TxnState> {
843        let state = self.live(txn.id)?.txn.state;
844        if state != TxnState::Reverting {
845            self.advance(txn, TxnState::Reverting, why)?;
846        }
847        let to = txn.revert_to;
848        let (op, activated) =
849            self.call_op(txn.id, "activate_live(revert_to)", |h, op| h.generations().activate_live(to, op))?;
850        if !activated.is_success() {
851            // Never decide from the observed state while the revert's own job may still run.
852            if let Err(why) = self.drain_op(txn.id, &op)? {
853                return self.recovery_failed(txn, &format!("revert activation: {why}"));
854            }
855        }
856        if !activated.is_success() && self.host.generations().current().ok() != Some(to) {
857            return self.recovery_failed(txn, &format!("revert activation: {activated:?}"));
858        }
859        self.restop_suppressed(txn)?;
860        let published = self.call(txn.id, "publish_boot(revert_to)", |h, op| h.generations().publish_boot(to, op))?;
861        if !published.is_success() {
862            return self.recovery_failed(txn, &format!("revert publish_boot: {published:?}"));
863        }
864        match self.swap_override(txn, txn.candidate_override, txn.base_override)? {
865            Swap::Done => {}
866            Swap::Failed(why) => return self.recovery_failed(txn, &format!("override restore: {why}")),
867            Swap::ThirdParty => return self.recovery_failed(txn, "override changed by a third party"),
868        }
869        match self.host.check(txn, CheckPhase::Revert) {
870            Verdict::Pass => {
871                self.advance(txn, TxnState::Reverted, why)?;
872                Ok(TxnState::Reverted)
873            }
874            Verdict::Fail(f) => self.recovery_failed(txn, &format!("revert probes: {f}")),
875        }
876    }
877
878    fn recovery_failed(&mut self, txn: &Transaction, why: &str) -> R<TxnState> {
879        self.advance(txn, TxnState::RecoveryFailed, why)?;
880        Ok(TxnState::RecoveryFailed)
881    }
882
883    fn run_device(&mut self, txn: &Transaction) -> R<TxnState> {
884        let Transition::BindDriver { device, driver } = &txn.transition else {
885            return self.reject(txn.id, RejectReason::Other("device op without BindDriver".into()));
886        };
887        self.advance(txn, TxnState::Activating, &format!("previous driver: {:?}", txn.prior_driver))?;
888        let (device, driver) = (device.clone(), driver.clone());
889        let bound = self.call(txn.id, "bind", |h, op| h.inventory().bind(&device, &driver, op))?;
890        if !bound.is_success() {
891            return self.revert_device(txn, &format!("bind: {bound:?}"));
892        }
893        self.advance(txn, TxnState::Checking, "")?;
894        self.check_device(txn)
895    }
896
897    fn check_device(&mut self, txn: &Transaction) -> R<TxnState> {
898        match self.host.check(txn, CheckPhase::Candidate) {
899            Verdict::Pass => {
900                self.advance(txn, TxnState::Committed, "")?;
901                Ok(TxnState::Committed)
902            }
903            Verdict::Fail(why) => self.revert_device(txn, &why),
904        }
905    }
906
907    /// Unbind and rebind the previously bound driver; never a generation rollback.
908    fn revert_device(&mut self, txn: &Transaction, why: &str) -> R<TxnState> {
909        let Transition::BindDriver { device, .. } = &txn.transition else { unreachable!("device op") };
910        let device = device.clone();
911        if self.live(txn.id)?.txn.state != TxnState::Reverting {
912            self.advance(txn, TxnState::Reverting, why)?;
913        }
914        let unbound = self.call(txn.id, "unbind", |h, op| h.inventory().unbind(&device, op))?;
915        if !unbound.is_success() && self.host.inventory().bound_driver(&device).ok() != Some(None) {
916            return self.recovery_failed(txn, &format!("unbind: {unbound:?}"));
917        }
918        if let Some(prev) = txn.prior_driver.clone() {
919            let rebound = self.call(txn.id, "bind(previous)", |h, op| h.inventory().bind(&device, &prev, op))?;
920            if !rebound.is_success() {
921                return self.recovery_failed(txn, &format!("rebind previous: {rebound:?}"));
922            }
923        }
924        match self.host.check(txn, CheckPhase::Revert) {
925            Verdict::Pass => {
926                self.advance(txn, TxnState::Reverted, why)?;
927                Ok(TxnState::Reverted)
928            }
929            Verdict::Fail(f) => self.recovery_failed(txn, &format!("revert probes: {f}")),
930        }
931    }
932
933    // ---------- Ownership ----------
934
935    /// Whether the watchdog should take this transaction over (§12 recovery watchdog).
936    pub fn watchdog_should_take_over(&self, txn: &Transaction) -> bool {
937        should_take_over(
938            &txn.state,
939            self.boot_only(txn.id),
940            txn.lease.owner == self.me,
941            // An owner from an earlier boot is gone, whatever now holds its pid.
942            txn.lease.owner.boot_id() == self.me.boot_id() && self.host.pid_alive(txn.lease.owner.pid()),
943            self.host.boot_now(),
944            txn.deadline,
945            txn.lease.renewed,
946        )
947    }
948
949    /// Take the lease (epoch + 1), fence the old owner, establish confirmed
950    /// quiescence. Returns the lease this handle now holds, or `Ok(None)` if
951    /// quiescence could not be confirmed (RecoveryFailed has been written).
952    /// `seen_epoch` is the epoch the caller based its decision on; if another
953    /// owner took over since, this one backs off with `LostLease`.
954    pub fn take_over(&mut self, id: TxnId, seen_epoch: u64) -> R<Option<Lease>> {
955        let now = self.host.now();
956        let me = self.me.clone();
957        let worker = self.worker_pid;
958        let (old, new) = self.journal.locked(|j| -> Result<(Lease, Lease), KernelError> {
959            let l = j.ledger().live.get(&id).cloned().ok_or(KernelError::NotLive(id))?;
960            let old = l.txn.lease.clone();
961            if old.epoch != seen_epoch {
962                return Err(KernelError::LostLease(id));
963            }
964            // The fence may already be ahead of the journal if a previous
965            // takeover crashed between the two writes.
966            let fence = j.store().fence(id).map_err(|e| KernelError::Journal(e.into()))?;
967            let epoch = next_epoch(old.epoch, fence).ok_or(KernelError::LostLease(id))?;
968            let new =
969                Lease { txn: id, owner: me, epoch, worker_pid: worker, renewed: self.host.boot_now(), jobs: vec![] };
970            // Fence first: from here on the old owner's calls are refused.
971            j.store().set_fence(id, epoch).map_err(|e| KernelError::Journal(e.into()))?;
972            j.append(now, Record::Takeover { txn: id, from_epoch: old.epoch, lease: new.clone() })?;
973            Ok((old, new))
974        })??;
975        self.held.insert(id, new.epoch);
976
977        let l = self.live(id)?;
978        if l.txn.state.is_pre_mutation() {
979            return Ok(Some(new));
980        }
981        match self.quiesce(&l, &old)? {
982            Ok(()) => Ok(Some(new)),
983            Err(why) => {
984                self.advance(&l.txn, TxnState::RecoveryFailed, &format!("quiescence not confirmed: {why}"))?;
985                Ok(None)
986            }
987        }
988    }
989
990    /// Kill the old worker; cancel every job in the old lease and every job
991    /// tagged with an unacknowledged OpId; wait for them to leave the queue.
992    fn quiesce(&mut self, l: &LiveTxn, old: &Lease) -> R<Result<(), String>> {
993        if let Some(pid) = old.worker_pid
994            && !self.host.kill_worker(pid)
995        {
996            return Ok(Err(format!("worker {pid} did not die")));
997        }
998        let mut jobs: Vec<JobId> = old.jobs.clone();
999        let unacked: Vec<OpId> = l.unacked_ops().map(|o| o.op).collect();
1000        for op in &unacked {
1001            match self.host.generations().jobs_tagged(op) {
1002                Ok(Some(found)) => jobs.extend(found),
1003                Ok(None) => return Ok(Err(format!("backend cannot say what op {op} started"))),
1004                Err(o) => return Ok(Err(format!("jobs_tagged({op}): {o:?}"))),
1005            }
1006        }
1007        jobs.sort();
1008        jobs.dedup();
1009        for job in &jobs {
1010            self.call(l.txn.id, "cancel", |h, op| h.generations().cancel(job, op))?;
1011        }
1012        if !self.wait_gone(&jobs)? {
1013            return Ok(Err(format!("jobs still pending after {}s drain", JOB_DRAIN.0 / 1000)));
1014        }
1015        self.supersede_unacked(&unacked)?;
1016        Ok(Ok(()))
1017    }
1018
1019    /// Acknowledge, as superseded by the takeover, each of `ops` that is
1020    /// still unacknowledged. Decided under the journal's lock: the old owner,
1021    /// fenced but not necessarily dead, may have journaled its own ack since
1022    /// the takeover (its call returned just then), and that one stands
1023    /// (run 109's `crash-slow-worker-overlap`: a second ack was `DuplicateOp`
1024    /// and left the transaction in flight).
1025    pub fn supersede_unacked(&mut self, ops: &[OpId]) -> R<()> {
1026        for op in ops {
1027            let now = self.host.now();
1028            let ack = Record::OpAck { op: *op, outcome: Outcome::Unknown("superseded by takeover".into()) };
1029            let appended = self.journal.locked(|j| {
1030                let still = j.ledger().live.get(&op.txn).is_some_and(|l| l.unacked_ops().any(|o| o.op == *op));
1031                if still { j.append(now, ack.clone()).map(|_| true) } else { Ok(false) }
1032            })?;
1033            match appended {
1034                Ok(_) => {}
1035                // A full journal: the sentinel path, as for any record.
1036                Err(JournalError::Full) => self.record(ack)?,
1037                Err(e) => return Err(e.into()),
1038            }
1039        }
1040        Ok(())
1041    }
1042
1043    // ---------- Recovery ----------
1044
1045    /// Take over (from `seen_epoch`) and run the crash-recovery table.
1046    pub fn recover(&mut self, id: TxnId, seen_epoch: u64) -> R<TxnState> {
1047        let before = self.live(id)?.txn;
1048        let same_boot = before.lease.owner.boot_id() == self.me.boot_id();
1049        if self.take_over(id, seen_epoch)?.is_none() {
1050            return Ok(TxnState::RecoveryFailed);
1051        }
1052        if before.state == TxnState::Promoted && self.boot_only(id) {
1053            let txn = self.live(id)?.txn;
1054            return self.readmit_boot_only(&txn, same_boot);
1055        }
1056        self.recover_steps(id)
1057    }
1058
1059    /// The crash-recovery table (§12), as `recovery_action` gives it.
1060    /// Assumes this handle holds the lease.
1061    fn recover_steps(&mut self, id: TxnId) -> R<TxnState> {
1062        let l = self.live(id)?;
1063        let txn = l.txn;
1064        use RecoveryAction as A;
1065        match recovery_action(&txn.state, txn.recovery) {
1066            A::Reject => self.reject(id, RejectReason::Interrupted),
1067            // Activating: a StopAndSuppress whose suppression is durable but whose
1068            // stop may not have happened: the suppression holds; re-observe and record.
1069            A::Reobserve => self.finish_reobserve(&txn),
1070            A::CandidateActive => {
1071                let cand = Self::candidate(&txn);
1072                if cand.is_some() && self.host.generations().current().ok() == cand {
1073                    self.advance(&txn, TxnState::Checking, "recovery: candidate is active")?;
1074                    self.check_then_commit(&txn)
1075                } else {
1076                    self.revert_generation(&txn, "recovery: candidate not active")
1077                }
1078            }
1079            A::CheckGeneration => self.check_then_commit(&txn),
1080            A::RecheckPublished => match self.host.check(&txn, CheckPhase::Candidate) {
1081                Verdict::Pass => {
1082                    let cand = Self::candidate(&txn).expect("candidate");
1083                    let o = self.call(id, "publish_boot", |h, op| h.generations().publish_boot(cand, op))?;
1084                    if !o.is_success() {
1085                        return self.revert_generation(&txn, &format!("publish_boot: {o:?}"));
1086                    }
1087                    self.promote_and_commit(&txn)
1088                }
1089                Verdict::Fail(why) => self.revert_generation(&txn, &why),
1090            },
1091            A::WriteCommitted => self.write_committed(&txn),
1092            A::RevertGeneration => self.revert_generation(&txn, "recovery: re-run revert"),
1093            A::DriverBound => {
1094                let Transition::BindDriver { device, driver } = &txn.transition else { unreachable!() };
1095                if self.host.inventory().bound_driver(device).ok().flatten().as_ref() == Some(driver) {
1096                    self.advance(&txn, TxnState::Checking, "recovery: candidate driver bound")?;
1097                    self.check_device(&txn)
1098                } else {
1099                    self.revert_device(&txn, "recovery: candidate driver not bound")
1100                }
1101            }
1102            A::CheckDevice => self.check_device(&txn),
1103            A::RevertDevice => self.revert_device(&txn, "recovery: re-run revert"),
1104            A::AwaitBoot => self.await_boot(&txn),
1105            A::Publishing => self.recover_publishing(&txn, &l.ops),
1106            A::Fail => {
1107                let why = format!("no recovery path for {} under {:?}", tag(&txn.state), txn.recovery);
1108                self.recovery_failed(&txn, &why)
1109            }
1110        }
1111    }
1112
1113    /// Daemon start (§12 daemon restart reconciliation): wait out transactions a
1114    /// live watchdog holds, recover the rest, and journal what was found and done.
1115    /// Returns the transactions left to a live watchdog.
1116    pub fn reconcile(&mut self) -> R<Vec<TxnId>> {
1117        Ok(self.reconcile_deferring(false)?.0)
1118    }
1119
1120    /// As `reconcile`, but with `defer`, a generation transaction found in a
1121    /// state whose recovery may run an activation (Activating, Checking,
1122    /// Publishing, Published, Reverting) is only taken over: its recovery is
1123    /// left to `finish_deferred`, which the daemon runs after ready, so an
1124    /// activation that reloads a unit ordered after planarian-ready.target
1125    /// does not wait on the daemon's own start (#9). The lease is this
1126    /// handle's from here, so the watchdog does not take it while ready is
1127    /// reached; the boot report and integrity checks still come before ready.
1128    /// Returns the transactions left to a live watchdog, and those deferred.
1129    pub fn reconcile_deferring(&mut self, defer: bool) -> R<(Vec<TxnId>, Vec<TxnId>)> {
1130        let live: Vec<LiveTxn> = self.journal.sync_ledger()?.live.values().cloned().collect();
1131        let mut found = vec![];
1132        let mut actions = vec![];
1133        let mut waiting = vec![];
1134        let mut deferred = vec![];
1135        for l in live {
1136            found.push((l.txn.id, l.txn.state.clone()));
1137            let holder = &l.txn.lease.owner;
1138            if holder.is_watchdog() && self.host.pid_alive(holder.pid()) {
1139                actions.push(format!("txn {}: held by the watchdog; waiting", l.txn.id.0));
1140                waiting.push(l.txn.id);
1141                continue;
1142            }
1143            let activates = matches!(
1144                l.txn.state,
1145                TxnState::Activating
1146                    | TxnState::Checking
1147                    | TxnState::Publishing
1148                    | TxnState::Published
1149                    | TxnState::Reverting
1150            );
1151            if defer && activates && l.txn.transition.changes_generation() {
1152                if self.take_over(l.txn.id, l.txn.lease.epoch)?.is_none() {
1153                    actions.push(format!("txn {}: found {}, takeover failed", l.txn.id.0, tag(&l.txn.state)));
1154                    continue;
1155                }
1156                actions.push(format!(
1157                    "txn {}: found {}, taken over; recovered after ready",
1158                    l.txn.id.0,
1159                    tag(&l.txn.state)
1160                ));
1161                deferred.push(l.txn.id);
1162                continue;
1163            }
1164            let end = self.recover(l.txn.id, l.txn.lease.epoch)?;
1165            actions.push(format!("txn {}: found {}, now {}", l.txn.id.0, tag(&l.txn.state), tag(&end)));
1166        }
1167        // Report what the watchdog finished while this daemon was down.
1168        let ledger = self.journal.ledger();
1169        for (id, t) in ledger.terminal.iter().rev().take(16) {
1170            if t.owner.is_watchdog() {
1171                found.push((*id, t.state.clone()));
1172                let why = if t.why.is_empty() { String::new() } else { format!("; because {}", t.why) };
1173                actions.push(format!(
1174                    "txn {}: the watchdog took over and finished it: {} (path {}){why}",
1175                    id.0,
1176                    tag(&t.state),
1177                    t.path.join(" → ")
1178                ));
1179            }
1180        }
1181        self.record(Record::Reconciliation(Reconciliation { by: self.me.clone(), found, actions }))?;
1182        Ok((waiting, deferred))
1183    }
1184
1185    /// After ready: the recoveries `reconcile_deferring` left, with this
1186    /// handle already holding their leases; journalled as one reconciliation.
1187    pub fn finish_deferred(&mut self, ids: &[TxnId]) -> R<()> {
1188        if ids.is_empty() {
1189            return Ok(());
1190        }
1191        let mut found = vec![];
1192        let mut actions = vec![];
1193        for &id in ids {
1194            let Ok(l) = self.live(id) else {
1195                actions.push(format!("txn {}: no longer live", id.0));
1196                continue;
1197            };
1198            found.push((id, l.txn.state.clone()));
1199            if l.txn.lease.owner != self.me {
1200                actions.push(format!("txn {}: lease lost before recovery", id.0));
1201                continue;
1202            }
1203            let end = self.recover_steps(id)?;
1204            actions.push(format!("txn {}: found {}, now {} (after ready)", id.0, tag(&l.txn.state), tag(&end)));
1205        }
1206        self.record(Record::Reconciliation(Reconciliation { by: self.me.clone(), found, actions }))
1207    }
1208}
1209
1210enum Swap {
1211    Done,
1212    Failed(String),
1213    ThirdParty,
1214}