meerkat_runtime/
state_machine.rs1use meerkat_core::lifecycle::RunId;
6
7use crate::runtime_state::{RuntimeState, RuntimeStateTransitionError};
8
9#[derive(Debug, Clone)]
11pub struct RuntimeStateMachine {
12 state: RuntimeState,
13 current_run_id: Option<RunId>,
14 pre_run_state: Option<RuntimeState>,
16}
17
18impl RuntimeStateMachine {
19 pub fn new() -> Self {
21 Self {
22 state: RuntimeState::Initializing,
23 current_run_id: None,
24 pre_run_state: None,
25 }
26 }
27
28 pub fn from_state(state: RuntimeState) -> Self {
30 Self {
31 state,
32 current_run_id: None,
33 pre_run_state: None,
34 }
35 }
36
37 pub fn state(&self) -> RuntimeState {
39 self.state
40 }
41
42 pub fn current_run_id(&self) -> Option<&RunId> {
44 self.current_run_id.as_ref()
45 }
46
47 pub fn is_idle(&self) -> bool {
49 self.state == RuntimeState::Idle
50 }
51
52 pub fn is_running(&self) -> bool {
54 self.state == RuntimeState::Running
55 }
56
57 pub fn can_process_queue(&self) -> bool {
61 self.state.can_process_queue()
62 }
63
64 pub fn transition(
66 &mut self,
67 next: RuntimeState,
68 ) -> Result<RuntimeState, RuntimeStateTransitionError> {
69 let from = self.state;
70 self.state.transition(next)?;
71
72 if from == RuntimeState::Running && next != RuntimeState::Running {
74 self.current_run_id = None;
75 self.pre_run_state = None;
76 }
77
78 Ok(from)
79 }
80
81 pub fn start_run(&mut self, run_id: RunId) -> Result<(), RuntimeStateTransitionError> {
86 let from = self.state;
87 self.state.transition(RuntimeState::Running)?;
88 self.pre_run_state = Some(from);
89 self.current_run_id = Some(run_id);
90 Ok(())
91 }
92
93 pub fn complete_run(&mut self) -> Result<RunId, RuntimeStateTransitionError> {
98 let return_to = match self.pre_run_state.take() {
99 Some(RuntimeState::Retired) => RuntimeState::Retired,
100 _ => RuntimeState::Idle,
101 };
102 self.state.transition(return_to)?;
103 self.current_run_id
104 .take()
105 .ok_or(RuntimeStateTransitionError {
106 from: RuntimeState::Running,
107 to: return_to,
108 })
109 }
110
111 pub fn initialize(&mut self) -> Result<(), RuntimeStateTransitionError> {
113 self.state.transition(RuntimeState::Idle)
114 }
115
116 pub fn reset_to_idle(&mut self) -> Result<Option<RuntimeState>, RuntimeStateTransitionError> {
122 let from = self.state;
123 match from {
124 RuntimeState::Idle => Ok(None),
125 RuntimeState::Running => Err(RuntimeStateTransitionError {
126 from: RuntimeState::Running,
127 to: RuntimeState::Idle,
128 }),
129 RuntimeState::Retired => {
130 self.state = RuntimeState::Idle;
131 self.current_run_id = None;
132 self.pre_run_state = None;
133 Ok(Some(from))
134 }
135 _ => {
136 self.state.transition(RuntimeState::Idle)?;
137 self.current_run_id = None;
138 self.pre_run_state = None;
139 Ok(Some(from))
140 }
141 }
142 }
143}
144
145impl Default for RuntimeStateMachine {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151#[cfg(test)]
152#[allow(clippy::unwrap_used)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn new_starts_initializing() {
158 let sm = RuntimeStateMachine::new();
159 assert_eq!(sm.state(), RuntimeState::Initializing);
160 assert!(sm.current_run_id().is_none());
161 }
162
163 #[test]
164 fn initialize_transitions_to_idle() {
165 let mut sm = RuntimeStateMachine::new();
166 sm.initialize().unwrap();
167 assert!(sm.is_idle());
168 }
169
170 #[test]
171 fn start_run_transitions_to_running() {
172 let mut sm = RuntimeStateMachine::new();
173 sm.initialize().unwrap();
174 let run_id = RunId::new();
175 sm.start_run(run_id.clone()).unwrap();
176 assert!(sm.is_running());
177 assert_eq!(sm.current_run_id(), Some(&run_id));
178 }
179
180 #[test]
181 fn complete_run_returns_to_idle() {
182 let mut sm = RuntimeStateMachine::new();
183 sm.initialize().unwrap();
184 let run_id = RunId::new();
185 sm.start_run(run_id.clone()).unwrap();
186 let completed_id = sm.complete_run().unwrap();
187 assert_eq!(completed_id, run_id);
188 assert!(sm.is_idle());
189 assert!(sm.current_run_id().is_none());
190 }
191
192 #[test]
193 fn transition_clears_run_id() {
194 let mut sm = RuntimeStateMachine::new();
195 sm.initialize().unwrap();
196 sm.start_run(RunId::new()).unwrap();
197 sm.transition(RuntimeState::Recovering).unwrap();
198 assert!(sm.current_run_id().is_none());
199 }
200
201 #[test]
202 fn from_state_recovery() {
203 let sm = RuntimeStateMachine::from_state(RuntimeState::Recovering);
204 assert_eq!(sm.state(), RuntimeState::Recovering);
205 }
206
207 #[test]
208 fn idle_running_idle_cycle() {
209 let mut sm = RuntimeStateMachine::new();
210 sm.initialize().unwrap();
211
212 for _ in 0..3 {
213 sm.start_run(RunId::new()).unwrap();
214 assert!(sm.is_running());
215 sm.complete_run().unwrap();
216 assert!(sm.is_idle());
217 }
218 }
219
220 #[test]
221 fn invalid_transition_rejected() {
222 let mut sm = RuntimeStateMachine::new();
223 assert!(sm.transition(RuntimeState::Running).is_err());
225 }
226
227 #[test]
228 fn retire_from_idle() {
229 let mut sm = RuntimeStateMachine::new();
230 sm.initialize().unwrap();
231 sm.transition(RuntimeState::Retired).unwrap();
232 assert_eq!(sm.state(), RuntimeState::Retired);
233 }
234
235 #[test]
236 fn stop_from_retired() {
237 let mut sm = RuntimeStateMachine::new();
238 sm.initialize().unwrap();
239 sm.transition(RuntimeState::Retired).unwrap();
240 sm.transition(RuntimeState::Stopped).unwrap();
241 assert!(sm.state().is_terminal());
242 }
243
244 #[test]
245 fn reset_from_retired_returns_to_idle() {
246 let mut sm = RuntimeStateMachine::new();
247 sm.initialize().unwrap();
248 sm.transition(RuntimeState::Retired).unwrap();
249 let from = sm.reset_to_idle().unwrap();
250 assert_eq!(from, Some(RuntimeState::Retired));
251 assert_eq!(sm.state(), RuntimeState::Idle);
252 assert!(sm.current_run_id().is_none());
253 }
254
255 #[test]
256 fn reset_rejected_while_running() {
257 let mut sm = RuntimeStateMachine::new();
258 sm.initialize().unwrap();
259 sm.start_run(RunId::new()).unwrap();
260 assert!(sm.reset_to_idle().is_err());
261 assert!(sm.is_running()); }
263
264 #[test]
265 fn destroy_from_idle() {
266 let mut sm = RuntimeStateMachine::new();
267 sm.initialize().unwrap();
268 sm.transition(RuntimeState::Destroyed).unwrap();
269 assert!(sm.state().is_terminal());
270 }
271
272 #[test]
273 fn recovering_to_running() {
274 let mut sm = RuntimeStateMachine::from_state(RuntimeState::Recovering);
275 sm.start_run(RunId::new()).unwrap();
276 assert!(sm.is_running());
277 }
278
279 #[test]
280 fn retired_drain_cycle() {
281 let mut sm = RuntimeStateMachine::new();
282 sm.initialize().unwrap();
283 sm.transition(RuntimeState::Retired).unwrap();
284 assert!(sm.can_process_queue());
285
286 let run_id = RunId::new();
288 sm.start_run(run_id.clone()).unwrap();
289 assert!(sm.is_running());
290
291 let completed = sm.complete_run().unwrap();
293 assert_eq!(completed, run_id);
294 assert_eq!(sm.state(), RuntimeState::Retired);
295 }
296
297 #[test]
298 fn idle_run_returns_to_idle() {
299 let mut sm = RuntimeStateMachine::new();
300 sm.initialize().unwrap();
301
302 let run_id = RunId::new();
303 sm.start_run(run_id.clone()).unwrap();
304 let completed = sm.complete_run().unwrap();
305 assert_eq!(completed, run_id);
306 assert_eq!(sm.state(), RuntimeState::Idle);
307 }
308
309 #[test]
310 fn can_process_queue_states() {
311 let sm_idle = RuntimeStateMachine::from_state(RuntimeState::Idle);
312 assert!(sm_idle.can_process_queue());
313
314 let sm_retired = RuntimeStateMachine::from_state(RuntimeState::Retired);
315 assert!(sm_retired.can_process_queue());
316
317 let sm_running = RuntimeStateMachine::from_state(RuntimeState::Running);
318 assert!(!sm_running.can_process_queue());
319
320 let sm_stopped = RuntimeStateMachine::from_state(RuntimeState::Stopped);
321 assert!(!sm_stopped.can_process_queue());
322 }
323}