use crate::error::ErrorClass;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum EngineError {
#[error("not found: {entity}")]
NotFound { entity: &'static str },
#[error("validation: {kind:?}: {detail}")]
Validation {
kind: ValidationKind,
detail: String,
},
#[error("contention: {0:?}")]
Contention(ContentionKind),
#[error("conflict: {0:?}")]
Conflict(ConflictKind),
#[error("state: {0:?}")]
State(StateKind),
#[error("bug: {0:?}")]
Bug(BugKind),
#[error("transport ({backend}): {source}")]
Transport {
backend: &'static str,
#[source]
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
#[error("unavailable: {op}")]
Unavailable { op: &'static str },
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidationKind {
InvalidInput,
CapabilityMismatch,
InvalidCapabilities,
InvalidPolicyJson,
PayloadTooLarge,
SignalLimitExceeded,
InvalidWaitpointKey,
WaitpointNotTokenBound,
RetentionLimitExceeded,
InvalidLeaseForSuspend,
InvalidDependency,
InvalidWaitpointForExecution,
InvalidBlockingReason,
InvalidOffset,
Unauthorized,
InvalidBudgetScope,
BudgetOverrideNotAllowed,
InvalidQuotaSpec,
InvalidKid,
InvalidSecretHex,
InvalidGraceMs,
InvalidTagKey,
InvalidFrameType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContentionKind {
UseClaimResumedExecution,
NotAResumedExecution,
ExecutionNotLeaseable,
LeaseConflict,
InvalidClaimGrant,
ClaimGrantExpired,
NoEligibleExecution,
WaitpointNotFound,
WaitpointPendingUseBufferScript,
StaleGraphRevision,
ExecutionNotActive {
terminal_outcome: String,
lease_epoch: String,
lifecycle_phase: String,
attempt_id: String,
},
ExecutionNotEligible,
ExecutionNotInEligibleSet,
ExecutionNotReclaimable,
NoActiveLease,
RateLimitExceeded,
ConcurrencyLimitExceeded,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConflictKind {
DependencyAlreadyExists { existing: crate::contracts::EdgeSnapshot },
CycleDetected,
SelfReferencingEdge,
ExecutionAlreadyInFlow,
WaitpointAlreadyExists,
BudgetAttachConflict,
QuotaAttachConflict,
RotationConflict(String),
ActiveAttemptExists,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum StateKind {
StaleLease,
LeaseExpired,
LeaseRevoked,
ExecutionNotSuspended,
AlreadySuspended,
WaitpointClosed,
TargetNotSignalable,
DuplicateSignal,
ResumeConditionNotMet,
WaitpointNotPending,
PendingWaitpointExpired,
WaitpointNotOpen,
ExecutionNotTerminal,
MaxReplaysExhausted,
StreamClosed,
StaleOwnerCannotAppend,
GrantAlreadyExists,
ExecutionNotInFlow,
FlowAlreadyTerminal,
DepsNotSatisfied,
NotBlockedByDeps,
NotRunnable,
Terminal,
BudgetExceeded,
BudgetSoftExceeded,
OkAlreadyApplied,
AttemptNotStarted,
AttemptAlreadyTerminal,
ExecutionNotEligibleForAttempt,
ReplayNotAllowed,
MaxRetriesExhausted,
StreamAlreadyClosed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BugKind {
AttemptNotInCreatedState,
}
impl EngineError {
pub fn class(&self) -> ErrorClass {
match self {
Self::NotFound { .. } => ErrorClass::Terminal,
Self::Validation { .. } => ErrorClass::Terminal,
Self::Contention(_) => ErrorClass::Retryable,
Self::Conflict(_) => ErrorClass::Terminal,
Self::State(StateKind::BudgetExceeded) => ErrorClass::Cooperative,
Self::State(
StateKind::ExecutionNotSuspended
| StateKind::AlreadySuspended
| StateKind::WaitpointClosed
| StateKind::DuplicateSignal
| StateKind::GrantAlreadyExists
| StateKind::OkAlreadyApplied
| StateKind::AttemptAlreadyTerminal
| StateKind::StreamAlreadyClosed
| StateKind::BudgetSoftExceeded
| StateKind::WaitpointNotOpen
| StateKind::WaitpointNotPending
| StateKind::PendingWaitpointExpired
| StateKind::NotBlockedByDeps
| StateKind::DepsNotSatisfied,
) => ErrorClass::Informational,
Self::State(_) => ErrorClass::Terminal,
Self::Bug(_) => ErrorClass::Bug,
Self::Transport { .. } => ErrorClass::Terminal,
Self::Unavailable { .. } => ErrorClass::Terminal,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn class_contention_is_retryable() {
let err = EngineError::Contention(ContentionKind::LeaseConflict);
assert_eq!(err.class(), ErrorClass::Retryable);
}
#[test]
fn class_budget_exceeded_is_cooperative() {
let err = EngineError::State(StateKind::BudgetExceeded);
assert_eq!(err.class(), ErrorClass::Cooperative);
}
#[test]
fn class_duplicate_signal_is_informational() {
let err = EngineError::State(StateKind::DuplicateSignal);
assert_eq!(err.class(), ErrorClass::Informational);
}
#[test]
fn class_bug_variant() {
let err = EngineError::Bug(BugKind::AttemptNotInCreatedState);
assert_eq!(err.class(), ErrorClass::Bug);
}
#[test]
fn class_transport_defaults_terminal() {
let raw = std::io::Error::other("simulated transport error");
let err = EngineError::Transport {
backend: "test",
source: Box::new(raw),
};
assert_eq!(err.class(), ErrorClass::Terminal);
}
#[test]
fn unavailable_is_terminal() {
assert_eq!(
EngineError::Unavailable { op: "foo" }.class(),
ErrorClass::Terminal
);
}
}