Skip to main content

tasktree/
lifecycle.rs

1//! Task lifecycle status and validated transitions.
2
3use serde::{
4    Deserialize,
5    Deserializer,
6    Serialize,
7};
8use thiserror::Error;
9
10/// Lifecycle status of a task.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum TaskStatus {
14    /// Task has been admitted but not yet queued or started.
15    Admitted,
16    /// Task is waiting in the admission queue.
17    Queued,
18    /// Task is actively executing.
19    Running,
20    /// The task has entered its irreversible sealing region.
21    Sealing,
22    /// Task completed successfully.
23    Done,
24    /// Task failed with an error.
25    Failed,
26    /// Task was cancelled before completion.
27    Cancelled,
28}
29
30impl TaskStatus {
31    /// Every lifecycle status in declaration order.
32    pub const ALL: [Self; 7] = [
33        Self::Admitted,
34        Self::Queued,
35        Self::Running,
36        Self::Sealing,
37        Self::Done,
38        Self::Failed,
39        Self::Cancelled,
40    ];
41
42    /// Returns `true` if this status is terminal.
43    #[must_use]
44    pub const fn is_terminal(self) -> bool {
45        matches!(self, Self::Done | Self::Failed | Self::Cancelled)
46    }
47
48    /// Returns a static string label for the status.
49    #[must_use]
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            Self::Admitted => "admitted",
53            Self::Queued => "queued",
54            Self::Running => "running",
55            Self::Sealing => "sealing",
56            Self::Done => "done",
57            Self::Failed => "failed",
58            Self::Cancelled => "cancelled",
59        }
60    }
61
62    /// Parse a status from its canonical label.
63    ///
64    /// # Errors
65    ///
66    /// Returns the unrecognized label when no variant matches.
67    pub fn parse_label(label: &str) -> Result<Self, String> {
68        Self::ALL
69            .into_iter()
70            .find(|status| status.as_str() == label)
71            .ok_or_else(|| format!("unknown task status {label:?}"))
72    }
73
74    /// Validate a lifecycle transition.
75    ///
76    /// # Errors
77    ///
78    /// Rejects every edge outside the task state machine.
79    pub fn validate_transition(self, next: Self) -> Result<(), TaskTransitionError> {
80        TaskTransition::try_new(self, next).map(|_| ())
81    }
82}
83
84/// An illegal task lifecycle transition.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
86#[error("invalid task transition from {from} to {to}", from = .from.as_str(), to = .to.as_str())]
87pub struct TaskTransitionError {
88    from: TaskStatus,
89    to: TaskStatus,
90}
91
92impl TaskTransitionError {
93    /// Return the current status.
94    #[must_use]
95    pub const fn from(self) -> TaskStatus {
96        self.from
97    }
98
99    /// Return the refused target status.
100    #[must_use]
101    pub const fn to(self) -> TaskStatus {
102        self.to
103    }
104}
105
106/// One validated lifecycle transition.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
108pub struct TaskTransition {
109    from: TaskStatus,
110    to: TaskStatus,
111}
112
113impl TaskTransition {
114    /// Validate and construct one state-machine edge.
115    ///
116    /// # Errors
117    ///
118    /// Rejects illegal transitions, including every transition out of a
119    /// terminal state.
120    pub fn try_new(from: TaskStatus, to: TaskStatus) -> Result<Self, TaskTransitionError> {
121        let valid = match from {
122            TaskStatus::Admitted => matches!(
123                to,
124                TaskStatus::Queued | TaskStatus::Running | TaskStatus::Cancelled
125            ),
126            TaskStatus::Queued => matches!(to, TaskStatus::Running | TaskStatus::Cancelled),
127            TaskStatus::Running => matches!(
128                to,
129                TaskStatus::Sealing | TaskStatus::Done | TaskStatus::Failed | TaskStatus::Cancelled
130            ),
131            TaskStatus::Sealing => matches!(to, TaskStatus::Done | TaskStatus::Failed),
132            TaskStatus::Done | TaskStatus::Failed | TaskStatus::Cancelled => false,
133        };
134        if !valid {
135            return Err(TaskTransitionError { from, to });
136        }
137        Ok(Self { from, to })
138    }
139
140    /// Return the current status.
141    #[must_use]
142    pub const fn from(self) -> TaskStatus {
143        self.from
144    }
145
146    /// Return the target status.
147    #[must_use]
148    pub const fn to(self) -> TaskStatus {
149        self.to
150    }
151}
152
153impl<'de> Deserialize<'de> for TaskTransition {
154    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
155    where
156        D: Deserializer<'de>,
157    {
158        #[derive(Deserialize)]
159        struct WireTransition {
160            from: TaskStatus,
161            to: TaskStatus,
162        }
163
164        let wire = WireTransition::deserialize(deserializer)?;
165        Self::try_new(wire.from, wire.to).map_err(serde::de::Error::custom)
166    }
167}