Skip to main content

meerkat_runtime/
runtime_state.rs

1//! §22 RuntimeState — the runtime's public state projection.
2//!
3//! Canonical transition legality lives in `RuntimeControlAuthority`.
4
5use serde::{Deserialize, Serialize};
6
7#[allow(dead_code)]
8fn can_transition(from: &RuntimeState, next: &RuntimeState) -> bool {
9    use RuntimeState::{
10        Attached, Destroyed, Idle, Initializing, Recovering, Retired, Running, Stopped,
11    };
12
13    matches!(
14        (from, next),
15        (Initializing, Idle | Stopped | Destroyed)
16            | (
17                Idle,
18                Attached | Running | Retired | Recovering | Stopped | Destroyed
19            )
20            | (
21                Attached,
22                Running | Idle | Retired | Recovering | Stopped | Destroyed
23            )
24            | (
25                Running,
26                Idle | Attached | Recovering | Retired | Stopped | Destroyed
27            )
28            | (Recovering, Idle | Attached | Running | Stopped | Destroyed)
29            | (Retired, Running | Stopped | Destroyed)
30    )
31}
32
33/// The state of a runtime instance.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36#[non_exhaustive]
37pub enum RuntimeState {
38    /// Initializing (first state after creation).
39    Initializing,
40    /// Idle — no executor attached, no run in progress, ready to accept input.
41    Idle,
42    /// Attached — executor attached, runtime loop alive, waiting for input.
43    Attached,
44    /// A run is in progress.
45    Running,
46    /// Recovering from a crash or error.
47    Recovering,
48    /// Retired — no longer accepting new input, draining existing.
49    Retired,
50    /// Permanently stopped (terminal).
51    Stopped,
52    /// Destroyed (terminal).
53    Destroyed,
54}
55
56impl RuntimeState {
57    /// Check if this is a terminal state.
58    pub fn is_terminal(&self) -> bool {
59        matches!(self, Self::Stopped | Self::Destroyed)
60    }
61
62    /// Check if the runtime can accept new input in this state.
63    pub fn can_accept_input(&self) -> bool {
64        matches!(self, Self::Idle | Self::Attached | Self::Running)
65    }
66
67    /// Check if the runtime can process queued inputs in this state.
68    pub fn can_process_queue(&self) -> bool {
69        matches!(self, Self::Idle | Self::Attached | Self::Retired)
70    }
71
72    /// Check if the runtime is in the Attached state.
73    pub fn is_attached(&self) -> bool {
74        matches!(self, Self::Attached)
75    }
76
77    /// Check if the runtime is Idle or Attached.
78    pub fn is_idle_or_attached(&self) -> bool {
79        matches!(self, Self::Idle | Self::Attached)
80    }
81}
82
83impl std::fmt::Display for RuntimeState {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match self {
86            Self::Initializing => write!(f, "initializing"),
87            Self::Idle => write!(f, "idle"),
88            Self::Attached => write!(f, "attached"),
89            Self::Running => write!(f, "running"),
90            Self::Recovering => write!(f, "recovering"),
91            Self::Retired => write!(f, "retired"),
92            Self::Stopped => write!(f, "stopped"),
93            Self::Destroyed => write!(f, "destroyed"),
94        }
95    }
96}
97
98/// Error when an invalid runtime state transition is attempted.
99#[derive(Debug, Clone, thiserror::Error)]
100#[error("Invalid runtime state transition: {from} -> {to}")]
101pub struct RuntimeStateTransitionError {
102    pub from: RuntimeState,
103    pub to: RuntimeState,
104}
105
106#[cfg(test)]
107#[allow(clippy::unwrap_used)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn terminal_states() {
113        assert!(RuntimeState::Stopped.is_terminal());
114        assert!(RuntimeState::Destroyed.is_terminal());
115        assert!(!RuntimeState::Initializing.is_terminal());
116        assert!(!RuntimeState::Idle.is_terminal());
117        assert!(!RuntimeState::Attached.is_terminal());
118        assert!(!RuntimeState::Running.is_terminal());
119        assert!(!RuntimeState::Recovering.is_terminal());
120        assert!(!RuntimeState::Retired.is_terminal());
121    }
122
123    #[test]
124    fn input_and_queue_capabilities() {
125        assert!(RuntimeState::Idle.can_accept_input());
126        assert!(RuntimeState::Attached.can_accept_input());
127        assert!(RuntimeState::Running.can_accept_input());
128        assert!(!RuntimeState::Retired.can_accept_input());
129
130        assert!(RuntimeState::Idle.can_process_queue());
131        assert!(RuntimeState::Attached.can_process_queue());
132        assert!(RuntimeState::Retired.can_process_queue());
133        assert!(!RuntimeState::Running.can_process_queue());
134    }
135
136    #[test]
137    fn attachment_predicates() {
138        assert!(RuntimeState::Attached.is_attached());
139        assert!(RuntimeState::Idle.is_idle_or_attached());
140        assert!(RuntimeState::Attached.is_idle_or_attached());
141        assert!(!RuntimeState::Running.is_idle_or_attached());
142    }
143
144    #[test]
145    fn transition_table_matches_spec_examples() {
146        assert!(can_transition(
147            &RuntimeState::Initializing,
148            &RuntimeState::Idle
149        ));
150        assert!(can_transition(&RuntimeState::Idle, &RuntimeState::Attached));
151        assert!(can_transition(
152            &RuntimeState::Attached,
153            &RuntimeState::Running
154        ));
155        assert!(can_transition(
156            &RuntimeState::Running,
157            &RuntimeState::Retired
158        ));
159        assert!(can_transition(
160            &RuntimeState::Retired,
161            &RuntimeState::Stopped
162        ));
163
164        assert!(!can_transition(&RuntimeState::Stopped, &RuntimeState::Idle));
165        assert!(!can_transition(
166            &RuntimeState::Destroyed,
167            &RuntimeState::Running
168        ));
169        assert!(!can_transition(&RuntimeState::Retired, &RuntimeState::Idle));
170    }
171
172    #[test]
173    fn transition_failure_shape_matches_runtime_error() {
174        let result = if can_transition(&RuntimeState::Stopped, &RuntimeState::Idle) {
175            Ok(())
176        } else {
177            Err(RuntimeStateTransitionError {
178                from: RuntimeState::Stopped,
179                to: RuntimeState::Idle,
180            })
181        };
182
183        assert!(result.is_err());
184        assert!(matches!(
185            result.unwrap_err(),
186            RuntimeStateTransitionError {
187                from: RuntimeState::Stopped,
188                to: RuntimeState::Idle
189            }
190        ));
191    }
192
193    #[test]
194    fn serde_roundtrip_all_states() {
195        for state in [
196            RuntimeState::Initializing,
197            RuntimeState::Idle,
198            RuntimeState::Attached,
199            RuntimeState::Running,
200            RuntimeState::Recovering,
201            RuntimeState::Retired,
202            RuntimeState::Stopped,
203            RuntimeState::Destroyed,
204        ] {
205            let json = serde_json::to_value(state).unwrap();
206            let parsed: RuntimeState = serde_json::from_value(json).unwrap();
207            assert_eq!(state, parsed);
208        }
209    }
210
211    #[test]
212    fn display() {
213        assert_eq!(RuntimeState::Idle.to_string(), "idle");
214        assert_eq!(RuntimeState::Attached.to_string(), "attached");
215        assert_eq!(RuntimeState::Running.to_string(), "running");
216        assert_eq!(RuntimeState::Destroyed.to_string(), "destroyed");
217    }
218}