// SPDX-License-Identifier: AGPL-3.0-or-later use std::sync::atomic::{AtomicU32, Ordering}; #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum State { Idle = 0, Starting = 1, Running = 2, Stopping = 3, Stopped = 4, } impl State { fn from_u32(v: u32) -> State { match v { 0 => State::Idle, 1 => State::Starting, 2 => State::Running, 3 => State::Stopping, _ => State::Stopped, } } } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum TransitionError { IllegalTransition, DoubleStart, StopBeforeStart, StartWhileStopping, } pub fn is_allowed(from: State, to: State) -> bool { match from { State::Idle => matches!(to, State::Starting | State::Stopped), State::Starting => matches!(to, State::Running | State::Stopped), State::Running => matches!(to, State::Stopping | State::Stopped), State::Stopping => matches!(to, State::Stopped), State::Stopped => false, } } pub struct Machine { state: AtomicU32, } impl Machine { pub fn new() -> Self { Self { state: AtomicU32::new(State::Idle as u32), } } pub fn current(&self) -> State { State::from_u32(self.state.load(Ordering::Acquire)) } fn cas(&self, from: State, to: State) -> Result<(), TransitionError> { if !is_allowed(from, to) { return Err(TransitionError::IllegalTransition); } self.state .compare_exchange(from as u32, to as u32, Ordering::AcqRel, Ordering::Acquire) .map(|_| ()) .map_err(|_| TransitionError::IllegalTransition) } pub fn request_start(&self) -> Result<(), TransitionError> { match self.current() { State::Idle => self.cas(State::Idle, State::Starting), State::Starting | State::Running => Err(TransitionError::DoubleStart), State::Stopping => Err(TransitionError::StartWhileStopping), State::Stopped => Err(TransitionError::IllegalTransition), } } pub fn mark_running(&self) -> Result<(), TransitionError> { self.cas(State::Starting, State::Running) } pub fn request_stop(&self) -> Result<(), TransitionError> { match self.current() { State::Running => self.cas(State::Running, State::Stopping), State::Idle => Err(TransitionError::StopBeforeStart), State::Starting | State::Stopping | State::Stopped => { Err(TransitionError::IllegalTransition) } } } pub fn mark_stopped(&self) -> Result<(), TransitionError> { self.cas(State::Stopping, State::Stopped) } pub fn cancel_idle(&self) -> Result<(), TransitionError> { self.cas(State::Idle, State::Stopped) } pub fn mark_fatal(&self) -> State { loop { let raw = self.state.load(Ordering::Acquire); let prev = State::from_u32(raw); if prev == State::Stopped { return prev; } if self .state .compare_exchange( raw, State::Stopped as u32, Ordering::AcqRel, Ordering::Acquire, ) .is_ok() { return prev; } } } } impl Default for Machine { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn is_allowed_exhaustive() { let all = [ State::Idle, State::Starting, State::Running, State::Stopping, State::Stopped, ]; let allowed: &[(State, State)] = &[ (State::Idle, State::Starting), (State::Idle, State::Stopped), (State::Starting, State::Running), (State::Starting, State::Stopped), (State::Running, State::Stopping), (State::Running, State::Stopped), (State::Stopping, State::Stopped), ]; for &from in &all { for &to in &all { let expected = allowed.iter().any(|p| p.0 == from && p.1 == to); assert_eq!(expected, is_allowed(from, to), "{:?} -> {:?}", from, to); } } } #[test] fn happy_path() { let m = Machine::new(); assert_eq!(State::Idle, m.current()); m.request_start().unwrap(); assert_eq!(State::Starting, m.current()); m.mark_running().unwrap(); assert_eq!(State::Running, m.current()); m.request_stop().unwrap(); assert_eq!(State::Stopping, m.current()); m.mark_stopped().unwrap(); assert_eq!(State::Stopped, m.current()); } #[test] fn double_start_rejected() { let m = Machine::new(); m.request_start().unwrap(); assert_eq!(Err(TransitionError::DoubleStart), m.request_start()); m.mark_running().unwrap(); assert_eq!(Err(TransitionError::DoubleStart), m.request_start()); } #[test] fn stop_before_start_rejected() { let m = Machine::new(); assert_eq!(Err(TransitionError::StopBeforeStart), m.request_stop()); } #[test] fn start_while_stopping_rejected() { let m = Machine::new(); m.request_start().unwrap(); m.mark_running().unwrap(); m.request_stop().unwrap(); assert_eq!(Err(TransitionError::StartWhileStopping), m.request_start()); } #[test] fn cancel_idle_short_circuits() { let m = Machine::new(); m.cancel_idle().unwrap(); assert_eq!(State::Stopped, m.current()); assert_eq!(Err(TransitionError::IllegalTransition), m.request_start()); } #[test] fn mark_fatal_forces_stopped() { for &start in &[ State::Idle, State::Starting, State::Running, State::Stopping, ] { let m = Machine::new(); m.state.store(start as u32, Ordering::Release); let prev = m.mark_fatal(); assert_eq!(start, prev); assert_eq!(State::Stopped, m.current()); } } #[test] fn mark_fatal_idempotent() { let m = Machine::new(); m.state.store(State::Stopped as u32, Ordering::Release); let prev = m.mark_fatal(); assert_eq!(State::Stopped, prev); assert_eq!(State::Stopped, m.current()); } }