nixfleet_state_machine/transitions/
mod.rs

1//! Transition dispatcher. Routes `(state, event)` to the per-source-state
2//! handler in a sibling module, after performing the universal pre-checks
3//! (seq monotonicity, basic structural validity).
4//!
5//! Each per-state module pattern-matches the events legal from that source
6//! state and returns either the new state + effects, or `IllegalForState`
7//! for events the §3 graph doesn't permit from that source. The runtime
8//! layer is expected to log + drop illegal events (out-of-order retransmits)
9//! and rely on `Replay-From` heartbeat drift-detection (RFC-0005 §4.3) for
10//! gap recovery.
11
12mod activating;
13mod converged;
14mod deferred;
15mod failed;
16mod pending;
17mod reverted;
18mod soaking;
19
20use chrono::{DateTime, Utc};
21use nixfleet_proto::RolloutPolicy;
22
23use crate::effect::Effect;
24use crate::error::TransitionError;
25use crate::event::Event;
26use crate::state::{HostRolloutState, HostState};
27
28pub(crate) fn dispatch(
29    state: HostRolloutState,
30    event: Event,
31    now: DateTime<Utc>,
32    policy: &RolloutPolicy,
33) -> Result<(HostRolloutState, Vec<Effect>), TransitionError> {
34    // Universal pre-check: event seq must be strictly greater than the last
35    // event we've seen for this (rollout, host). Equal seq = retransmit
36    // (idempotent at the runtime layer via (host, rollout, seq) dedup); less
37    // than = stale out-of-order arrival.
38    let got = event.seq();
39    if got <= state.last_event_seq {
40        return Err(TransitionError::SeqRegression {
41            got,
42            last: state.last_event_seq,
43            rollout_id: state.rollout_id.clone(),
44            hostname: state.hostname.clone(),
45        });
46    }
47
48    match state.state {
49        HostState::Pending => pending::handle(state, event, now, policy),
50        HostState::Activating => activating::handle(state, event, now, policy),
51        HostState::Deferred => deferred::handle(state, event, now, policy),
52        HostState::Soaking => soaking::handle(state, event, now, policy),
53        HostState::Failed => failed::handle(state, event, now, policy),
54        HostState::Converged => converged::handle(state, event, now, policy),
55        HostState::Reverted => reverted::handle(state, event, now, policy),
56    }
57}
58
59/// Shared utility — variant name for diagnostic strings.
60pub(crate) fn event_name(event: &Event) -> &'static str {
61    match event {
62        Event::LocalActivate { .. } => "LocalActivate",
63        Event::LocalActivationStarted { .. } => "LocalActivationStarted",
64        Event::LocalActivationCompleted { .. } => "LocalActivationCompleted",
65        Event::LocalActivationDeferred { .. } => "LocalActivationDeferred",
66        Event::LocalActivationFailed { .. } => "LocalActivationFailed",
67        Event::LocalProbeTopologyDeclared { .. } => "LocalProbeTopologyDeclared",
68        Event::LocalProbeObservedFirst { .. } => "LocalProbeObservedFirst",
69        Event::LocalProbeResult { .. } => "LocalProbeResult",
70        Event::LocalProbeFailureFirst { .. } => "LocalProbeFailureFirst",
71        Event::LocalSustainedFailureCrossed { .. } => "LocalSustainedFailureCrossed",
72        Event::LocalRollbackCompleted { .. } => "LocalRollbackCompleted",
73        Event::LocalConvergedReached { .. } => "LocalConvergedReached",
74        Event::RemoteDispatchAck { .. } => "RemoteDispatchAck",
75        Event::RemoteActivationStarted { .. } => "RemoteActivationStarted",
76        Event::RemoteActivationCompleted { .. } => "RemoteActivationCompleted",
77        Event::RemoteActivationDeferred { .. } => "RemoteActivationDeferred",
78        Event::RemoteActivationFailed { .. } => "RemoteActivationFailed",
79        Event::RemoteProbeTopologyDeclared { .. } => "RemoteProbeTopologyDeclared",
80        Event::RemoteProbeObservedFirst { .. } => "RemoteProbeObservedFirst",
81        Event::RemoteProbeResult { .. } => "RemoteProbeResult",
82        Event::RemoteProbeFailureFirst { .. } => "RemoteProbeFailureFirst",
83        Event::RemoteFailed { .. } => "RemoteFailed",
84        Event::RemoteRollbackComplete { .. } => "RemoteRollbackComplete",
85        Event::RemoteConverged { .. } => "RemoteConverged",
86    }
87}
88
89pub(crate) fn illegal(state: &HostRolloutState, event: &Event) -> TransitionError {
90    TransitionError::IllegalForState {
91        from: state.state,
92        event: event_name(event),
93        rollout_id: state.rollout_id.clone(),
94        hostname: state.hostname.clone(),
95    }
96}