Skip to main content

qubit_state_machine/
state_machine.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Immutable finite state machine rules and CAS-backed event triggering.
10
11use std::collections::{HashMap, HashSet};
12use std::fmt::Debug;
13use std::hash::Hash;
14
15use qubit_atomic::AtomicRef;
16use qubit_cas::{CasDecision, CasError, CasExecutor, CasSuccess};
17
18use crate::{StateMachineBuilder, StateMachineError, StateMachineResult, Transition};
19
20/// Immutable finite state machine rules.
21///
22/// `S` is the state type and `E` is the event type. Both should usually be
23/// small enum-like types. The state machine itself is immutable and can be
24/// shared across threads; mutable current state is kept in [`AtomicRef`] and
25/// updated through [`qubit_cas::CasExecutor`].
26///
27/// # Common usage
28///
29/// Define the valid states and events, build an immutable transition table, and
30/// keep each object's current state in an [`AtomicRef`].
31///
32/// ```
33/// use qubit_state_machine::{AtomicRef, StateMachine};
34///
35/// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
36/// enum JobState {
37///     Queued,
38///     Running,
39///     Succeeded,
40///     Failed,
41/// }
42///
43/// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
44/// enum JobEvent {
45///     Start,
46///     Complete,
47///     Fail,
48/// }
49///
50/// fn create_job_machine() -> StateMachine<JobState, JobEvent> {
51///     StateMachine::builder()
52///         .add_states(&[
53///             JobState::Queued,
54///             JobState::Running,
55///             JobState::Succeeded,
56///             JobState::Failed,
57///         ])
58///         .set_initial_state(JobState::Queued)
59///         .set_final_states(&[JobState::Succeeded, JobState::Failed])
60///         .add_transition(JobState::Queued, JobEvent::Start, JobState::Running)
61///         .add_transition(JobState::Running, JobEvent::Complete, JobState::Succeeded)
62///         .add_transition(JobState::Running, JobEvent::Fail, JobState::Failed)
63///         .build()
64///         .expect("job state machine should be valid")
65/// }
66///
67/// let machine = create_job_machine();
68/// let state = AtomicRef::from_value(JobState::Queued);
69///
70/// assert_eq!(machine.trigger(&state, JobEvent::Start).unwrap(), JobState::Running);
71/// assert_eq!(*state.load(), JobState::Running);
72///
73/// assert!(machine.try_trigger(&state, JobEvent::Complete));
74/// assert_eq!(*state.load(), JobState::Succeeded);
75/// ```
76#[derive(Debug, Clone)]
77pub struct StateMachine<S, E>
78where
79    S: Copy + Eq + Hash + Debug + 'static,
80    E: Copy + Eq + Hash + Debug + 'static,
81{
82    states: HashSet<S>,
83    initial_states: HashSet<S>,
84    final_states: HashSet<S>,
85    transitions: HashSet<Transition<S, E>>,
86    transition_map: HashMap<(S, E), S>,
87    cas_executor: CasExecutor<S, StateMachineError<S, E>>,
88}
89
90impl<S, E> StateMachine<S, E>
91where
92    S: Copy + Eq + Hash + Debug + 'static,
93    E: Copy + Eq + Hash + Debug + 'static,
94{
95    /// Creates a builder for immutable state machine rules.
96    ///
97    /// # Returns
98    /// A new empty [`StateMachineBuilder`].
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use qubit_state_machine::StateMachine;
104    ///
105    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
106    /// enum State {
107    ///     New,
108    /// }
109    ///
110    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
111    /// enum Event {
112    ///     Start,
113    /// }
114    ///
115    /// let machine = StateMachine::<State, Event>::builder()
116    ///     .add_state(State::New)
117    ///     .set_initial_state(State::New)
118    ///     .build()
119    ///     .expect("single-state machine should build");
120    /// assert!(machine.contains_state(State::New));
121    /// ```
122    pub fn builder() -> StateMachineBuilder<S, E> {
123        StateMachineBuilder::new()
124    }
125
126    /// Creates a state machine from validated builder parts.
127    ///
128    /// # Parameters
129    /// - `builder`: Builder containing validated states and terminal markers.
130    /// - `transitions`: Validated transition set.
131    /// - `transition_map`: Lookup table keyed by `(source, event)`.
132    ///
133    /// # Returns
134    /// An immutable state machine.
135    ///
136    /// This constructor does not validate input. Rule validation belongs to
137    /// [`StateMachineBuilder::build`].
138    pub(crate) fn new(
139        builder: StateMachineBuilder<S, E>,
140        transitions: HashSet<Transition<S, E>>,
141        transition_map: HashMap<(S, E), S>,
142    ) -> Self {
143        Self {
144            states: builder.states,
145            initial_states: builder.initial_states,
146            final_states: builder.final_states,
147            transitions,
148            transition_map,
149            cas_executor: CasExecutor::latency_first(),
150        }
151    }
152
153    /// Returns all registered states.
154    ///
155    /// # Returns
156    /// An immutable view of the registered state set.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// # use qubit_state_machine::StateMachine;
162    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
163    /// # enum State { New, Running }
164    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
165    /// # enum Event { Start }
166    /// # let machine = StateMachine::builder()
167    /// #     .add_states(&[State::New, State::Running])
168    /// #     .add_transition(State::New, Event::Start, State::Running)
169    /// #     .build()
170    /// #     .expect("rules should build");
171    /// assert!(machine.states().contains(&State::New));
172    /// assert_eq!(machine.states().len(), 2);
173    /// ```
174    pub const fn states(&self) -> &HashSet<S> {
175        &self.states
176    }
177
178    /// Returns all configured initial states.
179    ///
180    /// # Returns
181    /// An immutable view of the initial state set.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// # use qubit_state_machine::StateMachine;
187    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
188    /// # enum State { New, Running }
189    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
190    /// # enum Event { Start }
191    /// # let machine = StateMachine::builder()
192    /// #     .add_states(&[State::New, State::Running])
193    /// #     .set_initial_state(State::New)
194    /// #     .add_transition(State::New, Event::Start, State::Running)
195    /// #     .build()
196    /// #     .expect("rules should build");
197    /// assert!(machine.initial_states().contains(&State::New));
198    /// ```
199    pub const fn initial_states(&self) -> &HashSet<S> {
200        &self.initial_states
201    }
202
203    /// Returns all configured final states.
204    ///
205    /// # Returns
206    /// An immutable view of the final state set.
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// # use qubit_state_machine::StateMachine;
212    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
213    /// # enum State { New, Done }
214    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
215    /// # enum Event { Finish }
216    /// # let machine = StateMachine::builder()
217    /// #     .add_states(&[State::New, State::Done])
218    /// #     .set_final_state(State::Done)
219    /// #     .add_transition(State::New, Event::Finish, State::Done)
220    /// #     .build()
221    /// #     .expect("rules should build");
222    /// assert!(machine.final_states().contains(&State::Done));
223    /// ```
224    pub const fn final_states(&self) -> &HashSet<S> {
225        &self.final_states
226    }
227
228    /// Returns all registered transitions.
229    ///
230    /// # Returns
231    /// An immutable view of the transition set.
232    ///
233    /// # Examples
234    ///
235    /// ```
236    /// use qubit_state_machine::{StateMachine, Transition};
237    ///
238    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
239    /// enum State {
240    ///     New,
241    ///     Running,
242    /// }
243    ///
244    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
245    /// enum Event {
246    ///     Start,
247    /// }
248    ///
249    /// let machine = StateMachine::builder()
250    ///     .add_states(&[State::New, State::Running])
251    ///     .add_transition(State::New, Event::Start, State::Running)
252    ///     .build()
253    ///     .expect("rules should build");
254    ///
255    /// assert!(machine
256    ///     .transitions()
257    ///     .contains(&Transition::new(State::New, Event::Start, State::Running)));
258    /// ```
259    pub const fn transitions(&self) -> &HashSet<Transition<S, E>> {
260        &self.transitions
261    }
262
263    /// Tests whether a state is registered in this state machine.
264    ///
265    /// # Parameters
266    /// - `state`: State to test.
267    ///
268    /// # Returns
269    /// `true` if the state is registered.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// # use qubit_state_machine::StateMachine;
275    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
276    /// # enum State { New, Running, Detached }
277    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
278    /// # enum Event { Start }
279    /// # let machine = StateMachine::builder()
280    /// #     .add_states(&[State::New, State::Running])
281    /// #     .add_transition(State::New, Event::Start, State::Running)
282    /// #     .build()
283    /// #     .expect("rules should build");
284    /// assert!(machine.contains_state(State::Running));
285    /// assert!(!machine.contains_state(State::Detached));
286    /// ```
287    pub fn contains_state(&self, state: S) -> bool {
288        self.states.contains(&state)
289    }
290
291    /// Tests whether a state is configured as an initial state.
292    ///
293    /// # Parameters
294    /// - `state`: State to test.
295    ///
296    /// # Returns
297    /// `true` if the state is an initial state.
298    ///
299    /// # Examples
300    ///
301    /// ```
302    /// # use qubit_state_machine::StateMachine;
303    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
304    /// # enum State { New, Running }
305    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
306    /// # enum Event { Start }
307    /// # let machine = StateMachine::builder()
308    /// #     .add_states(&[State::New, State::Running])
309    /// #     .set_initial_state(State::New)
310    /// #     .add_transition(State::New, Event::Start, State::Running)
311    /// #     .build()
312    /// #     .expect("rules should build");
313    /// assert!(machine.is_initial_state(State::New));
314    /// assert!(!machine.is_initial_state(State::Running));
315    /// ```
316    pub fn is_initial_state(&self, state: S) -> bool {
317        self.initial_states.contains(&state)
318    }
319
320    /// Tests whether a state is configured as a final state.
321    ///
322    /// # Parameters
323    /// - `state`: State to test.
324    ///
325    /// # Returns
326    /// `true` if the state is a final state.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// # use qubit_state_machine::StateMachine;
332    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
333    /// # enum State { Running, Done }
334    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
335    /// # enum Event { Finish }
336    /// # let machine = StateMachine::builder()
337    /// #     .add_states(&[State::Running, State::Done])
338    /// #     .set_final_state(State::Done)
339    /// #     .add_transition(State::Running, Event::Finish, State::Done)
340    /// #     .build()
341    /// #     .expect("rules should build");
342    /// assert!(machine.is_final_state(State::Done));
343    /// assert!(!machine.is_final_state(State::Running));
344    /// ```
345    pub fn is_final_state(&self, state: S) -> bool {
346        self.final_states.contains(&state)
347    }
348
349    /// Looks up the target state for a source state and event.
350    ///
351    /// This method only queries rules; it does not modify any current-state
352    /// storage.
353    ///
354    /// # Parameters
355    /// - `source`: Source state.
356    /// - `event`: Event to apply.
357    ///
358    /// # Returns
359    /// `Some(target)` if a transition exists, or `None` otherwise.
360    ///
361    /// # Examples
362    ///
363    /// ```
364    /// # use qubit_state_machine::StateMachine;
365    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
366    /// # enum State { New, Running }
367    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
368    /// # enum Event { Start, Finish }
369    /// # let machine = StateMachine::builder()
370    /// #     .add_states(&[State::New, State::Running])
371    /// #     .add_transition(State::New, Event::Start, State::Running)
372    /// #     .build()
373    /// #     .expect("rules should build");
374    /// assert_eq!(
375    ///     machine.transition_target(State::New, Event::Start),
376    ///     Some(State::Running),
377    /// );
378    /// assert_eq!(machine.transition_target(State::New, Event::Finish), None);
379    /// ```
380    pub fn transition_target(&self, source: S, event: E) -> Option<S> {
381        self.transition_map.get(&(source, event)).copied()
382    }
383
384    /// Triggers an event and updates the provided atomic state reference.
385    ///
386    /// # Parameters
387    /// - `state`: Current state atomic reference.
388    /// - `event`: Event to apply.
389    ///
390    /// # Returns
391    /// The new state after a successful transition.
392    ///
393    /// # Errors
394    /// Returns [`StateMachineError::UnknownState`] when the current state is not
395    /// registered. Returns [`StateMachineError::UnknownTransition`] when the
396    /// current state is registered but has no transition for `event`.
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// use qubit_state_machine::{AtomicRef, StateMachine};
402    ///
403    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
404    /// enum State {
405    ///     New,
406    ///     Running,
407    /// }
408    ///
409    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
410    /// enum Event {
411    ///     Start,
412    /// }
413    ///
414    /// let machine = StateMachine::builder()
415    ///     .add_states(&[State::New, State::Running])
416    ///     .add_transition(State::New, Event::Start, State::Running)
417    ///     .build()
418    ///     .expect("rules should build");
419    /// let state = AtomicRef::from_value(State::New);
420    ///
421    /// assert_eq!(machine.trigger(&state, Event::Start).unwrap(), State::Running);
422    /// assert_eq!(*state.load(), State::Running);
423    /// ```
424    pub fn trigger(&self, state: &AtomicRef<S>, event: E) -> StateMachineResult<S, E> {
425        let (_, new_state) = self.change_state(state, event)?;
426        Ok(new_state)
427    }
428
429    /// Triggers an event, updates the atomic state, and invokes a success callback.
430    ///
431    /// The callback runs after the CAS update has succeeded.
432    ///
433    /// # Parameters
434    /// - `state`: Current state atomic reference.
435    /// - `event`: Event to apply.
436    /// - `on_success`: Callback receiving `(old_state, new_state)`.
437    ///
438    /// # Returns
439    /// The new state after a successful transition.
440    ///
441    /// # Errors
442    /// Returns the same errors as [`StateMachine::trigger`]. The callback is not
443    /// invoked when the transition fails.
444    ///
445    /// # Examples
446    ///
447    /// ```
448    /// use qubit_state_machine::{AtomicRef, StateMachine};
449    ///
450    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
451    /// enum State {
452    ///     New,
453    ///     Running,
454    /// }
455    ///
456    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
457    /// enum Event {
458    ///     Start,
459    /// }
460    ///
461    /// let machine = StateMachine::builder()
462    ///     .add_states(&[State::New, State::Running])
463    ///     .add_transition(State::New, Event::Start, State::Running)
464    ///     .build()
465    ///     .expect("rules should build");
466    /// let state = AtomicRef::from_value(State::New);
467    /// let mut observed = None;
468    ///
469    /// let next = machine
470    ///     .trigger_with(&state, Event::Start, |old_state, new_state| {
471    ///         observed = Some((old_state, new_state));
472    ///     })
473    ///     .expect("start should be valid");
474    ///
475    /// assert_eq!(next, State::Running);
476    /// assert_eq!(observed, Some((State::New, State::Running)));
477    /// ```
478    pub fn trigger_with<F>(
479        &self,
480        state: &AtomicRef<S>,
481        event: E,
482        on_success: F,
483    ) -> StateMachineResult<S, E>
484    where
485        F: FnOnce(S, S),
486    {
487        let (old_state, new_state) = self.change_state(state, event)?;
488        on_success(old_state, new_state);
489        Ok(new_state)
490    }
491
492    /// Attempts to trigger an event without returning error details.
493    ///
494    /// # Parameters
495    /// - `state`: Current state atomic reference.
496    /// - `event`: Event to apply.
497    ///
498    /// # Returns
499    /// `true` if the state changed successfully; `false` if the transition was
500    /// invalid.
501    ///
502    /// # Examples
503    ///
504    /// ```
505    /// use qubit_state_machine::{AtomicRef, StateMachine};
506    ///
507    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
508    /// enum State {
509    ///     New,
510    ///     Running,
511    /// }
512    ///
513    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
514    /// enum Event {
515    ///     Start,
516    ///     Finish,
517    /// }
518    ///
519    /// let machine = StateMachine::builder()
520    ///     .add_states(&[State::New, State::Running])
521    ///     .add_transition(State::New, Event::Start, State::Running)
522    ///     .build()
523    ///     .expect("rules should build");
524    /// let state = AtomicRef::from_value(State::New);
525    ///
526    /// assert!(!machine.try_trigger(&state, Event::Finish));
527    /// assert_eq!(*state.load(), State::New);
528    /// assert!(machine.try_trigger(&state, Event::Start));
529    /// ```
530    pub fn try_trigger(&self, state: &AtomicRef<S>, event: E) -> bool {
531        self.trigger(state, event).is_ok()
532    }
533
534    /// Attempts to trigger an event and invokes a callback only on success.
535    ///
536    /// # Parameters
537    /// - `state`: Current state atomic reference.
538    /// - `event`: Event to apply.
539    /// - `on_success`: Callback receiving `(old_state, new_state)`.
540    ///
541    /// # Returns
542    /// `true` if the state changed successfully; `false` if the transition was
543    /// invalid. The callback is skipped when this method returns `false`.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// use qubit_state_machine::{AtomicRef, StateMachine};
549    ///
550    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
551    /// enum State {
552    ///     New,
553    ///     Running,
554    /// }
555    ///
556    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
557    /// enum Event {
558    ///     Start,
559    ///     Finish,
560    /// }
561    ///
562    /// let machine = StateMachine::builder()
563    ///     .add_states(&[State::New, State::Running])
564    ///     .add_transition(State::New, Event::Start, State::Running)
565    ///     .build()
566    ///     .expect("rules should build");
567    /// let state = AtomicRef::from_value(State::New);
568    /// let mut callback_count = 0;
569    ///
570    /// assert!(!machine.try_trigger_with(&state, Event::Finish, |_, _| {
571    ///     callback_count += 1;
572    /// }));
573    /// assert_eq!(callback_count, 0);
574    ///
575    /// assert!(machine.try_trigger_with(&state, Event::Start, |_, _| {
576    ///     callback_count += 1;
577    /// }));
578    /// assert_eq!(callback_count, 1);
579    /// ```
580    pub fn try_trigger_with<F>(&self, state: &AtomicRef<S>, event: E, on_success: F) -> bool
581    where
582        F: FnOnce(S, S),
583    {
584        self.trigger_with(state, event, on_success).is_ok()
585    }
586
587    /// Applies a transition through the CAS executor.
588    ///
589    /// # Parameters
590    /// - `state`: Current state atomic reference.
591    /// - `event`: Event to apply.
592    ///
593    /// # Returns
594    /// The old and new state when the transition succeeds.
595    ///
596    /// # Errors
597    /// Returns a runtime state machine error when no valid next state exists.
598    fn change_state(
599        &self,
600        state: &AtomicRef<S>,
601        event: E,
602    ) -> Result<(S, S), StateMachineError<S, E>> {
603        let outcome = self.cas_executor.execute(state, |current_state: &S| {
604            match self.next_state(*current_state, event) {
605                Ok(new_state) => CasDecision::update(new_state, new_state),
606                Err(error) => CasDecision::abort(error),
607            }
608        });
609        match outcome.into_result() {
610            Ok(success) => Ok(Self::state_change_from_success(success)),
611            Err(error) => Err(Self::state_error_from_cas_error(error)),
612        }
613    }
614
615    /// Resolves the next state for the current state and event.
616    ///
617    /// # Parameters
618    /// - `current_state`: State currently stored by the atomic reference.
619    /// - `event`: Event to apply.
620    ///
621    /// # Returns
622    /// The target state when a transition exists.
623    ///
624    /// # Errors
625    /// Returns an unknown-state error before checking transitions if the current
626    /// state is not registered. Returns an unknown-transition error if no rule
627    /// exists for the `(current_state, event)` pair.
628    fn next_state(&self, current_state: S, event: E) -> Result<S, StateMachineError<S, E>> {
629        if !self.contains_state(current_state) {
630            return Err(StateMachineError::UnknownState {
631                state: current_state,
632            });
633        }
634        self.transition_target(current_state, event)
635            .ok_or(StateMachineError::UnknownTransition {
636                source: current_state,
637                event,
638            })
639    }
640
641    /// Extracts old and new states from a successful CAS transition.
642    ///
643    /// # Parameters
644    /// - `success`: Successful CAS result returned by the executor.
645    ///
646    /// # Returns
647    /// The old state and current state after CAS completion.
648    fn state_change_from_success(success: CasSuccess<S, S>) -> (S, S) {
649        match success {
650            CasSuccess::Updated {
651                previous, current, ..
652            } => (*previous, *current),
653            CasSuccess::Finished { current, .. } => (*current, *current),
654        }
655    }
656
657    /// Maps terminal CAS failures into state machine errors.
658    ///
659    /// # Parameters
660    /// - `error`: Terminal CAS error returned by the executor.
661    ///
662    /// # Returns
663    /// The business state machine error when the operation aborted, or a CAS
664    /// conflict error when retry limits were exhausted by compare-and-swap
665    /// conflicts.
666    fn state_error_from_cas_error(
667        error: CasError<S, StateMachineError<S, E>>,
668    ) -> StateMachineError<S, E> {
669        match error.error() {
670            Some(error) => *error,
671            None => StateMachineError::CasConflict {
672                attempts: error.attempts(),
673            },
674        }
675    }
676}