use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LifecyclePhase {
Submitted,
Runnable,
Active,
Suspended,
Terminal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OwnershipState {
Unowned,
Leased,
LeaseExpiredReclaimable,
LeaseRevoked,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EligibilityState {
EligibleNow,
NotEligibleUntilTime,
BlockedByDependencies,
BlockedByBudget,
BlockedByQuota,
BlockedByRoute,
BlockedByLaneState,
BlockedByOperator,
NotApplicable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockingReason {
None,
WaitingForWorker,
WaitingForRetryBackoff,
WaitingForResumeDelay,
WaitingForDelay,
WaitingForSignal,
WaitingForApproval,
WaitingForCallback,
WaitingForToolResult,
WaitingForChildren,
WaitingForBudget,
WaitingForQuota,
WaitingForCapableWorker,
WaitingForLocalityMatch,
PausedByOperator,
PausedByPolicy,
PausedByFlowCancel,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TerminalOutcome {
None,
Success,
Failed,
Cancelled,
Expired,
Skipped,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AttemptState {
None,
PendingFirstAttempt,
RunningAttempt,
AttemptInterrupted,
PendingRetryAttempt,
PendingReplayAttempt,
AttemptTerminal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PublicState {
Waiting,
Delayed,
RateLimited,
WaitingChildren,
Active,
Suspended,
Resumable,
Completed,
Failed,
Cancelled,
Expired,
Skipped,
}
impl PublicState {
pub fn as_str(self) -> &'static str {
match self {
Self::Waiting => "waiting",
Self::Delayed => "delayed",
Self::RateLimited => "rate_limited",
Self::WaitingChildren => "waiting_children",
Self::Active => "active",
Self::Suspended => "suspended",
Self::Resumable => "resumable",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Cancelled => "cancelled",
Self::Expired => "expired",
Self::Skipped => "skipped",
}
}
}
impl fmt::Display for PublicState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateVector {
pub lifecycle_phase: LifecyclePhase,
pub ownership_state: OwnershipState,
pub eligibility_state: EligibilityState,
pub blocking_reason: BlockingReason,
pub terminal_outcome: TerminalOutcome,
pub attempt_state: AttemptState,
pub public_state: PublicState,
}
impl StateVector {
pub fn derive_public_state(&self) -> PublicState {
match self.lifecycle_phase {
LifecyclePhase::Terminal => match self.terminal_outcome {
TerminalOutcome::Success => PublicState::Completed,
TerminalOutcome::Failed => PublicState::Failed,
TerminalOutcome::Cancelled => PublicState::Cancelled,
TerminalOutcome::Expired => PublicState::Expired,
TerminalOutcome::Skipped => PublicState::Skipped,
TerminalOutcome::None => {
PublicState::Failed
}
},
LifecyclePhase::Suspended => PublicState::Suspended,
LifecyclePhase::Active => PublicState::Active,
LifecyclePhase::Runnable => match self.eligibility_state {
EligibilityState::EligibleNow => PublicState::Waiting,
EligibilityState::NotEligibleUntilTime => PublicState::Delayed,
EligibilityState::BlockedByDependencies => PublicState::WaitingChildren,
EligibilityState::BlockedByBudget | EligibilityState::BlockedByQuota => {
PublicState::RateLimited
}
EligibilityState::BlockedByRoute
| EligibilityState::BlockedByLaneState
| EligibilityState::BlockedByOperator => PublicState::Waiting,
EligibilityState::NotApplicable => {
PublicState::Waiting
}
},
LifecyclePhase::Submitted => PublicState::Waiting,
}
}
pub fn is_consistent(&self) -> bool {
self.public_state == self.derive_public_state()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AttemptLifecycle {
Created,
Started,
Suspended,
EndedSuccess,
EndedFailure,
EndedCancelled,
InterruptedReclaimed,
}
impl AttemptLifecycle {
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::EndedSuccess
| Self::EndedFailure
| Self::EndedCancelled
| Self::InterruptedReclaimed
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AttemptType {
Initial,
Retry,
Reclaim,
Replay,
Fallback,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LaneState {
Active,
Paused,
Draining,
Disabled,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_public_state_terminal_success() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Terminal,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::NotApplicable,
blocking_reason: BlockingReason::None,
terminal_outcome: TerminalOutcome::Success,
attempt_state: AttemptState::AttemptTerminal,
public_state: PublicState::Completed,
};
assert_eq!(sv.derive_public_state(), PublicState::Completed);
assert!(sv.is_consistent());
}
#[test]
fn derive_public_state_active() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Active,
ownership_state: OwnershipState::Leased,
eligibility_state: EligibilityState::NotApplicable,
blocking_reason: BlockingReason::None,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::RunningAttempt,
public_state: PublicState::Active,
};
assert_eq!(sv.derive_public_state(), PublicState::Active);
assert!(sv.is_consistent());
}
#[test]
fn derive_public_state_active_lease_expired_still_active() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Active,
ownership_state: OwnershipState::LeaseExpiredReclaimable,
eligibility_state: EligibilityState::NotApplicable,
blocking_reason: BlockingReason::None,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::AttemptInterrupted,
public_state: PublicState::Active,
};
assert_eq!(sv.derive_public_state(), PublicState::Active);
}
#[test]
fn derive_public_state_runnable_eligible() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Runnable,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::EligibleNow,
blocking_reason: BlockingReason::WaitingForWorker,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::PendingFirstAttempt,
public_state: PublicState::Waiting,
};
assert_eq!(sv.derive_public_state(), PublicState::Waiting);
}
#[test]
fn derive_public_state_delayed() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Runnable,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::NotEligibleUntilTime,
blocking_reason: BlockingReason::WaitingForRetryBackoff,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::PendingRetryAttempt,
public_state: PublicState::Delayed,
};
assert_eq!(sv.derive_public_state(), PublicState::Delayed);
}
#[test]
fn derive_public_state_waiting_children() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Runnable,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::BlockedByDependencies,
blocking_reason: BlockingReason::WaitingForChildren,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::PendingFirstAttempt,
public_state: PublicState::WaitingChildren,
};
assert_eq!(sv.derive_public_state(), PublicState::WaitingChildren);
}
#[test]
fn derive_public_state_rate_limited() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Runnable,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::BlockedByBudget,
blocking_reason: BlockingReason::WaitingForBudget,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::PendingFirstAttempt,
public_state: PublicState::RateLimited,
};
assert_eq!(sv.derive_public_state(), PublicState::RateLimited);
}
#[test]
fn derive_public_state_suspended() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Suspended,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::NotApplicable,
blocking_reason: BlockingReason::WaitingForApproval,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::AttemptInterrupted,
public_state: PublicState::Suspended,
};
assert_eq!(sv.derive_public_state(), PublicState::Suspended);
}
#[test]
fn derive_public_state_submitted_collapses_to_waiting() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Submitted,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::NotApplicable,
blocking_reason: BlockingReason::None,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::None,
public_state: PublicState::Waiting,
};
assert_eq!(sv.derive_public_state(), PublicState::Waiting);
}
#[test]
fn derive_public_state_skipped() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Terminal,
ownership_state: OwnershipState::Unowned,
eligibility_state: EligibilityState::NotApplicable,
blocking_reason: BlockingReason::None,
terminal_outcome: TerminalOutcome::Skipped,
attempt_state: AttemptState::None,
public_state: PublicState::Skipped,
};
assert_eq!(sv.derive_public_state(), PublicState::Skipped);
}
#[test]
fn attempt_lifecycle_terminal_check() {
assert!(AttemptLifecycle::EndedSuccess.is_terminal());
assert!(AttemptLifecycle::EndedFailure.is_terminal());
assert!(AttemptLifecycle::EndedCancelled.is_terminal());
assert!(AttemptLifecycle::InterruptedReclaimed.is_terminal());
assert!(!AttemptLifecycle::Created.is_terminal());
assert!(!AttemptLifecycle::Started.is_terminal());
assert!(!AttemptLifecycle::Suspended.is_terminal());
}
#[test]
fn public_state_resumable_roundtrip_display_and_serde() {
assert_eq!(PublicState::Resumable.to_string(), "resumable");
assert_eq!(PublicState::Resumable.as_str(), "resumable");
let json = serde_json::to_string(&PublicState::Resumable).unwrap();
assert_eq!(json, "\"resumable\"");
let parsed: PublicState = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, PublicState::Resumable);
}
#[test]
fn serde_roundtrip_lifecycle_phase() {
let phase = LifecyclePhase::Active;
let json = serde_json::to_string(&phase).unwrap();
assert_eq!(json, "\"active\"");
let parsed: LifecyclePhase = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, phase);
}
#[test]
fn serde_roundtrip_blocking_reason() {
let reason = BlockingReason::PausedByFlowCancel;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"paused_by_flow_cancel\"");
let parsed: BlockingReason = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, reason);
}
#[test]
fn serde_roundtrip_state_vector() {
let sv = StateVector {
lifecycle_phase: LifecyclePhase::Active,
ownership_state: OwnershipState::Leased,
eligibility_state: EligibilityState::NotApplicable,
blocking_reason: BlockingReason::None,
terminal_outcome: TerminalOutcome::None,
attempt_state: AttemptState::RunningAttempt,
public_state: PublicState::Active,
};
let json = serde_json::to_string(&sv).unwrap();
let parsed: StateVector = serde_json::from_str(&json).unwrap();
assert_eq!(sv, parsed);
}
}