nixfleet_state_machine/lib.rs
1#![forbid(unsafe_code)]
2//! Pure per-host rollout state-machine reducer.
3//!
4//! Implements RFC-0005 §3 (6-state machine) and RFC-0006 §3 (functional
5//! core / imperative shell). The reducer is a single function:
6//!
7//! ```ignore
8//! step(state, event, now, policy) -> Result<(state, Vec<Effect>), TransitionError>
9//! ```
10//!
11//! Properties enforced by the crate's structure:
12//!
13//! - **No I/O.** `Cargo.toml` declares no tokio, no reqwest, no rusqlite.
14//! CI verifies via `cargo tree -p nixfleet-state-machine` (RFC-0006 §11).
15//! - **No clock reads.** `chrono::Utc::now()` is not called anywhere in
16//! this crate; `now` is always a parameter.
17//! - **Pure transitions.** `step` is deterministic for a given input.
18//! Same `(state, event, now, policy)` → same `(state, effects)`.
19//! - **Side effects as data.** `Effect` is a description, not an execution.
20//! The agent runtime (RFC-0006 §7.1) and CP runtime (§7.2) each
21//! exhaustively match on `Effect` variants; adding a variant is a
22//! compiler-enforced change at every applier.
23//!
24//! ## Same code, both sides
25//!
26//! The CP-mirror view of per-host state (RFC-0006 §2 principle 4) runs the
27//! same `step()` as the agent. `Event` carries both `Local*` (agent-side,
28//! synthesized from worker output) and `Remote*` (CP-side, synthesized from
29//! inbound agent events) variants — the transitions they drive are identical
30//! by construction.
31
32mod effect;
33mod error;
34mod event;
35mod rehydration;
36mod state;
37mod transitions;
38mod wire_conversions;
39
40pub mod rollout;
41
42pub use effect::{Effect, LogLevel, OutboundAgentEvent};
43pub use error::TransitionError;
44pub use event::{Event, ProbeTopologyEntry};
45pub use rehydration::rehydration_effects;
46pub use state::{
47 ClosureHash, HostRolloutState, HostState, ProbeMode, ProbeName, ProbeRecord, ProbeStatus,
48 ProbeSubResult, RolloutId,
49};
50
51use chrono::{DateTime, Utc};
52use nixfleet_proto::RolloutPolicy;
53
54/// Pure reducer. Same signature on agent and CP-mirror sides.
55///
56/// `now` is a parameter so tests can advance time deterministically and the
57/// reducer cannot drift from any caller's notion of "current time" by
58/// hitting a global clock.
59///
60/// `policy` is borrowed from the signed manifest the caller already verified
61/// (via [`nixfleet_reconciler::verify_rollout_manifest`]). The reducer cannot
62/// fetch it from a different source.
63pub fn step(
64 state: HostRolloutState,
65 event: Event,
66 now: DateTime<Utc>,
67 policy: &RolloutPolicy,
68) -> Result<(HostRolloutState, Vec<Effect>), TransitionError> {
69 transitions::dispatch(state, event, now, policy)
70}