1pub trait StateMachine: Copy + Eq + std::fmt::Debug + Sized + 'static {
12 const STATES: &'static [Self];
14 const EDGES: &'static [(Self, Self)];
16 const ESCAPE: &'static [Self];
22
23 fn is_terminal(self) -> bool {
26 !Self::EDGES.iter().any(|(from, _)| *from == self)
27 }
28
29 fn can_transition(from: Self, to: Self) -> bool {
30 Self::EDGES.contains(&(from, to))
31 }
32}
33
34macro_rules! edges {
35 ($ty:ident: $($from:ident -> $to:ident),+ $(,)?) => {
36 &[$(($ty::$from, $ty::$to)),+]
37 };
38}
39
40#[derive(
42 Debug,
43 Clone,
44 Copy,
45 PartialEq,
46 Eq,
47 PartialOrd,
48 Ord,
49 Hash,
50 serde::Serialize,
51 serde::Deserialize,
52 specta::Type,
53)]
54#[serde(rename_all = "snake_case")]
55pub enum TaskState {
56 Submitted,
57 Working,
58 InputRequired,
59 Completed,
60 Failed,
61 Canceled,
62}
63
64impl StateMachine for TaskState {
65 const STATES: &'static [Self] = &[
66 Self::Submitted,
67 Self::Working,
68 Self::InputRequired,
69 Self::Completed,
70 Self::Failed,
71 Self::Canceled,
72 ];
73 const EDGES: &'static [(Self, Self)] = edges![TaskState:
74 Submitted -> Working,
75 Submitted -> Canceled,
76 Working -> InputRequired,
77 Working -> Completed,
78 Working -> Failed,
79 Working -> Canceled,
80 InputRequired -> Working,
81 InputRequired -> Canceled,
82 ];
83 const ESCAPE: &'static [Self] = &[Self::Canceled, Self::Failed];
84}
85
86#[derive(
89 Debug,
90 Clone,
91 Copy,
92 PartialEq,
93 Eq,
94 PartialOrd,
95 Ord,
96 Hash,
97 serde::Serialize,
98 serde::Deserialize,
99 specta::Type,
100)]
101#[serde(rename_all = "snake_case")]
102pub enum AttemptState {
103 Queued,
104 Leased,
105 Starting,
106 Running,
107 Blocked,
108 Canceling,
109 Canceled,
110 Failed,
111 Unknown,
112 Succeeded,
113}
114
115impl StateMachine for AttemptState {
116 const STATES: &'static [Self] = &[
117 Self::Queued,
118 Self::Leased,
119 Self::Starting,
120 Self::Running,
121 Self::Blocked,
122 Self::Canceling,
123 Self::Canceled,
124 Self::Failed,
125 Self::Unknown,
126 Self::Succeeded,
127 ];
128 const EDGES: &'static [(Self, Self)] = edges![AttemptState:
129 Queued -> Leased,
130 Queued -> Canceled,
131 Leased -> Starting,
132 Leased -> Canceled,
133 Starting -> Running,
134 Starting -> Failed,
135 Starting -> Unknown,
136 Starting -> Canceling,
137 Running -> Blocked,
140 Running -> Canceling,
141 Running -> Failed,
142 Running -> Unknown,
143 Running -> Succeeded,
144 Blocked -> Running,
145 Blocked -> Canceling,
146 Blocked -> Failed,
147 Blocked -> Unknown,
148 Canceling -> Canceled,
149 Canceling -> Unknown,
150 ];
151 const ESCAPE: &'static [Self] = &[Self::Canceling, Self::Canceled, Self::Failed, Self::Unknown];
152}
153
154#[derive(
156 Debug,
157 Clone,
158 Copy,
159 PartialEq,
160 Eq,
161 PartialOrd,
162 Ord,
163 Hash,
164 serde::Serialize,
165 serde::Deserialize,
166 specta::Type,
167)]
168#[serde(rename_all = "snake_case")]
169pub enum MessageState {
170 Accepted,
171 Delivered,
172 Acknowledged,
173 Applied,
174 Rejected,
175 DeadLetter,
176}
177
178impl StateMachine for MessageState {
179 const STATES: &'static [Self] = &[
180 Self::Accepted,
181 Self::Delivered,
182 Self::Acknowledged,
183 Self::Applied,
184 Self::Rejected,
185 Self::DeadLetter,
186 ];
187 const EDGES: &'static [(Self, Self)] = edges![MessageState:
188 Accepted -> Delivered,
189 Accepted -> DeadLetter,
190 Delivered -> Acknowledged,
191 Delivered -> DeadLetter,
192 Acknowledged -> Applied,
193 Acknowledged -> Rejected,
194 Acknowledged -> DeadLetter,
195 ];
196 const ESCAPE: &'static [Self] = &[Self::DeadLetter];
197}
198
199#[derive(
203 Debug,
204 Clone,
205 Copy,
206 PartialEq,
207 Eq,
208 PartialOrd,
209 Ord,
210 Hash,
211 serde::Serialize,
212 serde::Deserialize,
213 specta::Type,
214)]
215#[serde(rename_all = "snake_case")]
216pub enum CommandState {
217 Issued,
218 Targeted,
219 Signaled,
220 VerificationComplete,
221}
222
223impl StateMachine for CommandState {
224 const STATES: &'static [Self] = &[
225 Self::Issued,
226 Self::Targeted,
227 Self::Signaled,
228 Self::VerificationComplete,
229 ];
230 const EDGES: &'static [(Self, Self)] = edges![CommandState:
231 Issued -> Targeted,
232 Targeted -> Signaled,
233 Signaled -> VerificationComplete,
234 ];
235 const ESCAPE: &'static [Self] = &[];
238}
239
240#[derive(
243 Debug,
244 Clone,
245 Copy,
246 PartialEq,
247 Eq,
248 PartialOrd,
249 Ord,
250 Hash,
251 serde::Serialize,
252 serde::Deserialize,
253 specta::Type,
254)]
255#[serde(rename_all = "snake_case")]
256pub enum Outcome {
257 Clean,
258 Partial,
259 Unknown,
260}
261
262#[derive(
264 Debug,
265 Clone,
266 Copy,
267 PartialEq,
268 Eq,
269 PartialOrd,
270 Ord,
271 Hash,
272 serde::Serialize,
273 serde::Deserialize,
274 specta::Type,
275)]
276#[serde(rename_all = "snake_case")]
277pub enum GateVerdict {
278 Pending,
279 Pass,
280 Fail,
281 Partial,
282}
283
284#[derive(
287 Debug,
288 Clone,
289 Copy,
290 PartialEq,
291 Eq,
292 PartialOrd,
293 Ord,
294 Hash,
295 serde::Serialize,
296 serde::Deserialize,
297 specta::Type,
298)]
299#[serde(rename_all = "snake_case")]
300pub enum LeaseState {
301 Held,
302 Released,
303 Expired,
304}
305
306#[derive(
308 Debug,
309 Clone,
310 Copy,
311 PartialEq,
312 Eq,
313 PartialOrd,
314 Ord,
315 Hash,
316 serde::Serialize,
317 serde::Deserialize,
318 specta::Type,
319)]
320#[serde(rename_all = "snake_case")]
321pub enum LeaseMode {
322 Exclusive,
323 Shared,
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn terminality_is_derived_from_edges() {
332 assert!(TaskState::Completed.is_terminal());
333 assert!(!TaskState::Submitted.is_terminal());
334 assert!(AttemptState::Unknown.is_terminal());
335 assert!(!AttemptState::Canceling.is_terminal());
336 assert!(MessageState::DeadLetter.is_terminal());
337 assert!(CommandState::VerificationComplete.is_terminal());
338 }
339
340 #[test]
341 fn wire_values_are_snake_case() {
342 assert_eq!(
343 serde_json::to_value(TaskState::InputRequired).expect("serialize"),
344 serde_json::json!("input_required")
345 );
346 assert_eq!(
347 serde_json::to_value(CommandState::VerificationComplete).expect("serialize"),
348 serde_json::json!("verification_complete")
349 );
350 assert_eq!(
351 serde_json::to_value(MessageState::DeadLetter).expect("serialize"),
352 serde_json::json!("dead_letter")
353 );
354 }
355}