Skip to main content

actionqueue_core/run/
transitions.rs

1//! State transition table for run instances.
2//!
3//! This module defines the valid transitions between run states and
4//! enforces the invariant that backward transitions are forbidden.
5
6use crate::run::state::RunState;
7
8/// Typed errors for lifecycle transition construction.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum RunTransitionError {
11    /// The `(from -> to)` transition is not allowed by the canonical transition table.
12    InvalidTransition {
13        /// Source lifecycle state.
14        from: RunState,
15        /// Target lifecycle state.
16        to: RunState,
17    },
18}
19
20impl std::fmt::Display for RunTransitionError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            Self::InvalidTransition { from, to } => {
24                write!(f, "invalid run transition: {from:?} -> {to:?}")
25            }
26        }
27    }
28}
29
30impl std::error::Error for RunTransitionError {}
31
32/// A transition represents a valid state change.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[must_use]
35pub struct Transition {
36    from: RunState,
37    to: RunState,
38}
39
40impl Transition {
41    /// Creates a new transition from `from` to `to`.
42    /// Returns a typed error if the transition is not valid.
43    pub fn new(from: RunState, to: RunState) -> Result<Self, RunTransitionError> {
44        if is_valid_transition(from, to) {
45            Ok(Transition { from, to })
46        } else {
47            Err(RunTransitionError::InvalidTransition { from, to })
48        }
49    }
50
51    /// Returns the source state of this transition.
52    pub fn from(&self) -> RunState {
53        self.from
54    }
55
56    /// Returns the target state of this transition.
57    pub fn to(&self) -> RunState {
58        self.to
59    }
60}
61
62/// Checks if a transition from `from` to `to` is valid.
63///
64/// Valid transitions:
65/// - Scheduled -> Ready
66/// - Scheduled -> Canceled
67/// - Ready -> Leased
68/// - Ready -> Canceled
69/// - Leased -> Running
70/// - Leased -> Ready (lease expired)
71/// - Leased -> Canceled
72/// - Running -> RetryWait (failure with retries remaining)
73/// - Running -> Suspended (preempted by budget exhaustion)
74/// - Running -> Completed (success)
75/// - Running -> Failed (failure, no retries remaining)
76/// - Running -> Canceled
77/// - RetryWait -> Ready (backoff complete)
78/// - RetryWait -> Failed (no more retries)
79/// - RetryWait -> Canceled
80/// - Suspended -> Ready (budget replenished / explicit resume)
81/// - Suspended -> Canceled
82///
83/// Terminal states (Completed, Failed, Canceled) have no valid transitions.
84pub fn is_valid_transition(from: RunState, to: RunState) -> bool {
85    // Terminal states cannot transition to any other state
86    if from.is_terminal() {
87        return false;
88    }
89
90    matches!(
91        (from, to),
92        (RunState::Scheduled, RunState::Ready)
93            | (RunState::Scheduled, RunState::Canceled)
94            | (RunState::Ready, RunState::Leased)
95            | (RunState::Ready, RunState::Canceled)
96            | (RunState::Leased, RunState::Running)
97            | (RunState::Leased, RunState::Ready)
98            | (RunState::Leased, RunState::Canceled)
99            | (RunState::Running, RunState::RetryWait)
100            | (RunState::Running, RunState::Suspended)
101            | (RunState::Running, RunState::Completed)
102            | (RunState::Running, RunState::Failed)
103            | (RunState::Running, RunState::Canceled)
104            | (RunState::RetryWait, RunState::Ready)
105            | (RunState::RetryWait, RunState::Failed)
106            | (RunState::RetryWait, RunState::Canceled)
107            | (RunState::Suspended, RunState::Ready)
108            | (RunState::Suspended, RunState::Canceled)
109    )
110}
111
112/// Returns all valid transitions from a given state.
113pub fn valid_transitions(from: RunState) -> Vec<RunState> {
114    let states = [
115        RunState::Scheduled,
116        RunState::Ready,
117        RunState::Leased,
118        RunState::Running,
119        RunState::RetryWait,
120        RunState::Suspended,
121        RunState::Completed,
122        RunState::Failed,
123        RunState::Canceled,
124    ];
125
126    states.into_iter().filter(|&to| is_valid_transition(from, to)).collect()
127}