1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum LifecyclePhase {
11 Submitted,
13 Runnable,
15 Active,
17 Suspended,
19 Terminal,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum OwnershipState {
29 Unowned,
31 Leased,
33 LeaseExpiredReclaimable,
35 LeaseRevoked,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum EligibilityState {
45 EligibleNow,
47 NotEligibleUntilTime,
49 BlockedByDependencies,
51 BlockedByBudget,
53 BlockedByQuota,
55 BlockedByRoute,
57 BlockedByLaneState,
59 BlockedByOperator,
61 NotApplicable,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum BlockingReason {
71 None,
73 WaitingForWorker,
75 WaitingForRetryBackoff,
77 WaitingForResumeDelay,
79 WaitingForDelay,
81 WaitingForSignal,
83 WaitingForApproval,
85 WaitingForCallback,
87 WaitingForToolResult,
89 WaitingForChildren,
91 WaitingForBudget,
93 WaitingForQuota,
95 WaitingForCapableWorker,
97 WaitingForLocalityMatch,
99 PausedByOperator,
101 PausedByPolicy,
103 PausedByFlowCancel,
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum TerminalOutcome {
113 None,
115 Success,
117 Failed,
119 Cancelled,
121 Expired,
123 Skipped,
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132pub enum AttemptState {
133 None,
135 PendingFirstAttempt,
137 RunningAttempt,
139 AttemptInterrupted,
141 PendingRetryAttempt,
143 PendingReplayAttempt,
145 AttemptTerminal,
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154#[non_exhaustive]
155pub enum PublicState {
156 Waiting,
158 Delayed,
160 RateLimited,
162 WaitingChildren,
164 Active,
166 Suspended,
168 Resumable,
174 Completed,
176 Failed,
178 Cancelled,
180 Expired,
182 Skipped,
184}
185
186impl PublicState {
187 pub fn as_str(self) -> &'static str {
191 match self {
192 Self::Waiting => "waiting",
193 Self::Delayed => "delayed",
194 Self::RateLimited => "rate_limited",
195 Self::WaitingChildren => "waiting_children",
196 Self::Active => "active",
197 Self::Suspended => "suspended",
198 Self::Resumable => "resumable",
199 Self::Completed => "completed",
200 Self::Failed => "failed",
201 Self::Cancelled => "cancelled",
202 Self::Expired => "expired",
203 Self::Skipped => "skipped",
204 }
205 }
206}
207
208impl fmt::Display for PublicState {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.write_str(self.as_str())
211 }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
218pub struct StateVector {
219 pub lifecycle_phase: LifecyclePhase,
220 pub ownership_state: OwnershipState,
221 pub eligibility_state: EligibilityState,
222 pub blocking_reason: BlockingReason,
223 pub terminal_outcome: TerminalOutcome,
224 pub attempt_state: AttemptState,
225 pub public_state: PublicState,
227}
228
229impl StateVector {
230 pub fn derive_public_state(&self) -> PublicState {
236 match self.lifecycle_phase {
237 LifecyclePhase::Terminal => match self.terminal_outcome {
238 TerminalOutcome::Success => PublicState::Completed,
239 TerminalOutcome::Failed => PublicState::Failed,
240 TerminalOutcome::Cancelled => PublicState::Cancelled,
241 TerminalOutcome::Expired => PublicState::Expired,
242 TerminalOutcome::Skipped => PublicState::Skipped,
243 TerminalOutcome::None => {
244 PublicState::Failed
249 }
250 },
251 LifecyclePhase::Suspended => PublicState::Suspended,
252 LifecyclePhase::Active => PublicState::Active,
253 LifecyclePhase::Runnable => match self.eligibility_state {
254 EligibilityState::EligibleNow => PublicState::Waiting,
255 EligibilityState::NotEligibleUntilTime => PublicState::Delayed,
256 EligibilityState::BlockedByDependencies => PublicState::WaitingChildren,
257 EligibilityState::BlockedByBudget | EligibilityState::BlockedByQuota => {
258 PublicState::RateLimited
259 }
260 EligibilityState::BlockedByRoute
261 | EligibilityState::BlockedByLaneState
262 | EligibilityState::BlockedByOperator => PublicState::Waiting,
263 EligibilityState::NotApplicable => {
264 PublicState::Waiting
267 }
268 },
269 LifecyclePhase::Submitted => PublicState::Waiting,
270 }
271 }
272
273 pub fn is_consistent(&self) -> bool {
275 self.public_state == self.derive_public_state()
276 }
277}
278
279#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
283#[serde(rename_all = "snake_case")]
284pub enum AttemptLifecycle {
285 Created,
287 Started,
289 Suspended,
291 EndedSuccess,
293 EndedFailure,
295 EndedCancelled,
297 InterruptedReclaimed,
299}
300
301impl AttemptLifecycle {
302 pub fn is_terminal(self) -> bool {
304 matches!(
305 self,
306 Self::EndedSuccess
307 | Self::EndedFailure
308 | Self::EndedCancelled
309 | Self::InterruptedReclaimed
310 )
311 }
312}
313
314#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
318#[serde(rename_all = "snake_case")]
319pub enum AttemptType {
320 Initial,
322 Retry,
324 Reclaim,
326 Replay,
328 Fallback,
330}
331
332#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case")]
337pub enum LaneState {
338 Active,
340 Paused,
342 Draining,
344 Disabled,
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn derive_public_state_terminal_success() {
354 let sv = StateVector {
355 lifecycle_phase: LifecyclePhase::Terminal,
356 ownership_state: OwnershipState::Unowned,
357 eligibility_state: EligibilityState::NotApplicable,
358 blocking_reason: BlockingReason::None,
359 terminal_outcome: TerminalOutcome::Success,
360 attempt_state: AttemptState::AttemptTerminal,
361 public_state: PublicState::Completed,
362 };
363 assert_eq!(sv.derive_public_state(), PublicState::Completed);
364 assert!(sv.is_consistent());
365 }
366
367 #[test]
368 fn derive_public_state_active() {
369 let sv = StateVector {
370 lifecycle_phase: LifecyclePhase::Active,
371 ownership_state: OwnershipState::Leased,
372 eligibility_state: EligibilityState::NotApplicable,
373 blocking_reason: BlockingReason::None,
374 terminal_outcome: TerminalOutcome::None,
375 attempt_state: AttemptState::RunningAttempt,
376 public_state: PublicState::Active,
377 };
378 assert_eq!(sv.derive_public_state(), PublicState::Active);
379 assert!(sv.is_consistent());
380 }
381
382 #[test]
383 fn derive_public_state_active_lease_expired_still_active() {
384 let sv = StateVector {
386 lifecycle_phase: LifecyclePhase::Active,
387 ownership_state: OwnershipState::LeaseExpiredReclaimable,
388 eligibility_state: EligibilityState::NotApplicable,
389 blocking_reason: BlockingReason::None,
390 terminal_outcome: TerminalOutcome::None,
391 attempt_state: AttemptState::AttemptInterrupted,
392 public_state: PublicState::Active,
393 };
394 assert_eq!(sv.derive_public_state(), PublicState::Active);
395 }
396
397 #[test]
398 fn derive_public_state_runnable_eligible() {
399 let sv = StateVector {
400 lifecycle_phase: LifecyclePhase::Runnable,
401 ownership_state: OwnershipState::Unowned,
402 eligibility_state: EligibilityState::EligibleNow,
403 blocking_reason: BlockingReason::WaitingForWorker,
404 terminal_outcome: TerminalOutcome::None,
405 attempt_state: AttemptState::PendingFirstAttempt,
406 public_state: PublicState::Waiting,
407 };
408 assert_eq!(sv.derive_public_state(), PublicState::Waiting);
409 }
410
411 #[test]
412 fn derive_public_state_delayed() {
413 let sv = StateVector {
414 lifecycle_phase: LifecyclePhase::Runnable,
415 ownership_state: OwnershipState::Unowned,
416 eligibility_state: EligibilityState::NotEligibleUntilTime,
417 blocking_reason: BlockingReason::WaitingForRetryBackoff,
418 terminal_outcome: TerminalOutcome::None,
419 attempt_state: AttemptState::PendingRetryAttempt,
420 public_state: PublicState::Delayed,
421 };
422 assert_eq!(sv.derive_public_state(), PublicState::Delayed);
423 }
424
425 #[test]
426 fn derive_public_state_waiting_children() {
427 let sv = StateVector {
428 lifecycle_phase: LifecyclePhase::Runnable,
429 ownership_state: OwnershipState::Unowned,
430 eligibility_state: EligibilityState::BlockedByDependencies,
431 blocking_reason: BlockingReason::WaitingForChildren,
432 terminal_outcome: TerminalOutcome::None,
433 attempt_state: AttemptState::PendingFirstAttempt,
434 public_state: PublicState::WaitingChildren,
435 };
436 assert_eq!(sv.derive_public_state(), PublicState::WaitingChildren);
437 }
438
439 #[test]
440 fn derive_public_state_rate_limited() {
441 let sv = StateVector {
442 lifecycle_phase: LifecyclePhase::Runnable,
443 ownership_state: OwnershipState::Unowned,
444 eligibility_state: EligibilityState::BlockedByBudget,
445 blocking_reason: BlockingReason::WaitingForBudget,
446 terminal_outcome: TerminalOutcome::None,
447 attempt_state: AttemptState::PendingFirstAttempt,
448 public_state: PublicState::RateLimited,
449 };
450 assert_eq!(sv.derive_public_state(), PublicState::RateLimited);
451 }
452
453 #[test]
454 fn derive_public_state_suspended() {
455 let sv = StateVector {
456 lifecycle_phase: LifecyclePhase::Suspended,
457 ownership_state: OwnershipState::Unowned,
458 eligibility_state: EligibilityState::NotApplicable,
459 blocking_reason: BlockingReason::WaitingForApproval,
460 terminal_outcome: TerminalOutcome::None,
461 attempt_state: AttemptState::AttemptInterrupted,
462 public_state: PublicState::Suspended,
463 };
464 assert_eq!(sv.derive_public_state(), PublicState::Suspended);
465 }
466
467 #[test]
468 fn derive_public_state_submitted_collapses_to_waiting() {
469 let sv = StateVector {
470 lifecycle_phase: LifecyclePhase::Submitted,
471 ownership_state: OwnershipState::Unowned,
472 eligibility_state: EligibilityState::NotApplicable,
473 blocking_reason: BlockingReason::None,
474 terminal_outcome: TerminalOutcome::None,
475 attempt_state: AttemptState::None,
476 public_state: PublicState::Waiting,
477 };
478 assert_eq!(sv.derive_public_state(), PublicState::Waiting);
479 }
480
481 #[test]
482 fn derive_public_state_skipped() {
483 let sv = StateVector {
484 lifecycle_phase: LifecyclePhase::Terminal,
485 ownership_state: OwnershipState::Unowned,
486 eligibility_state: EligibilityState::NotApplicable,
487 blocking_reason: BlockingReason::None,
488 terminal_outcome: TerminalOutcome::Skipped,
489 attempt_state: AttemptState::None,
490 public_state: PublicState::Skipped,
491 };
492 assert_eq!(sv.derive_public_state(), PublicState::Skipped);
493 }
494
495 #[test]
496 fn attempt_lifecycle_terminal_check() {
497 assert!(AttemptLifecycle::EndedSuccess.is_terminal());
498 assert!(AttemptLifecycle::EndedFailure.is_terminal());
499 assert!(AttemptLifecycle::EndedCancelled.is_terminal());
500 assert!(AttemptLifecycle::InterruptedReclaimed.is_terminal());
501 assert!(!AttemptLifecycle::Created.is_terminal());
502 assert!(!AttemptLifecycle::Started.is_terminal());
503 assert!(!AttemptLifecycle::Suspended.is_terminal());
504 }
505
506 #[test]
507 fn public_state_resumable_roundtrip_display_and_serde() {
508 assert_eq!(PublicState::Resumable.to_string(), "resumable");
510 assert_eq!(PublicState::Resumable.as_str(), "resumable");
511
512 let json = serde_json::to_string(&PublicState::Resumable).unwrap();
514 assert_eq!(json, "\"resumable\"");
515 let parsed: PublicState = serde_json::from_str(&json).unwrap();
516 assert_eq!(parsed, PublicState::Resumable);
517 }
518
519 #[test]
520 fn serde_roundtrip_lifecycle_phase() {
521 let phase = LifecyclePhase::Active;
522 let json = serde_json::to_string(&phase).unwrap();
523 assert_eq!(json, "\"active\"");
524 let parsed: LifecyclePhase = serde_json::from_str(&json).unwrap();
525 assert_eq!(parsed, phase);
526 }
527
528 #[test]
529 fn serde_roundtrip_blocking_reason() {
530 let reason = BlockingReason::PausedByFlowCancel;
531 let json = serde_json::to_string(&reason).unwrap();
532 assert_eq!(json, "\"paused_by_flow_cancel\"");
533 let parsed: BlockingReason = serde_json::from_str(&json).unwrap();
534 assert_eq!(parsed, reason);
535 }
536
537 #[test]
538 fn serde_roundtrip_state_vector() {
539 let sv = StateVector {
540 lifecycle_phase: LifecyclePhase::Active,
541 ownership_state: OwnershipState::Leased,
542 eligibility_state: EligibilityState::NotApplicable,
543 blocking_reason: BlockingReason::None,
544 terminal_outcome: TerminalOutcome::None,
545 attempt_state: AttemptState::RunningAttempt,
546 public_state: PublicState::Active,
547 };
548 let json = serde_json::to_string(&sv).unwrap();
549 let parsed: StateVector = serde_json::from_str(&json).unwrap();
550 assert_eq!(sv, parsed);
551 }
552}