Skip to main content

a3s_flow/
error.rs

1use std::fmt;
2
3use thiserror::Error;
4
5/// Crate-local result type.
6pub type Result<T> = std::result::Result<T, FlowError>;
7
8/// Errors surfaced by the workflow engine and runtime adapters.
9#[derive(Error)]
10pub enum FlowError {
11    #[error("workflow run not found: {0}")]
12    RunNotFound(String),
13
14    #[error("workflow run {0} is already terminal")]
15    RunTerminal(String),
16
17    #[error("workflow run id is invalid: {0}")]
18    InvalidRunId(String),
19
20    #[error("workflow run {run_id} conflicts with existing run: {reason}")]
21    RunConflict { run_id: String, reason: String },
22
23    #[error("non-deterministic workflow replay for run {run_id}: {reason}")]
24    NonDeterministic { run_id: String, reason: String },
25
26    #[error(
27        "event sequence conflict for run {run_id}: expected {expected_sequence}, actual {actual_sequence}"
28    )]
29    EventConflict {
30        run_id: String,
31        expected_sequence: u64,
32        actual_sequence: u64,
33    },
34
35    /// The original token remains available for programmatic routing, while
36    /// `Display` and `Debug` deliberately redact it.
37    #[error("active hook token not found (value redacted)")]
38    HookTokenNotFound(String),
39
40    #[error("workflow task lease is no longer active: {0}")]
41    LeaseLost(String),
42
43    /// The conflicting token remains available for programmatic handling,
44    /// while `Display` and `Debug` deliberately redact it.
45    #[error(
46        "active hook token is already used by run {existing_run_id} hook {existing_hook_id} (value redacted)"
47    )]
48    HookTokenConflict {
49        token: String,
50        existing_run_id: String,
51        existing_hook_id: String,
52    },
53
54    #[error("invalid workflow definition: {0}")]
55    InvalidWorkflow(String),
56
57    #[error("invalid state transition: {0}")]
58    InvalidTransition(String),
59
60    #[error("invalid worker configuration: {0}")]
61    InvalidWorkerConfiguration(String),
62
63    #[error("task manager error: {0}")]
64    TaskManagement(String),
65
66    #[error("event store error: {0}")]
67    Store(String),
68
69    #[error("runtime error: {0}")]
70    Runtime(String),
71
72    #[error("serialization error: {0}")]
73    Serialization(#[from] serde_json::Error),
74
75    #[error("io error: {0}")]
76    Io(#[from] std::io::Error),
77
78    #[error("workflow replay exceeded {0} iterations")]
79    ReplayLimitExceeded(usize),
80}
81
82// Error values can retain callback tokens for programmatic recovery, but
83// diagnostics must never reveal those bearer credentials. Keep ordinary
84// variants structurally useful while replacing token fields in Debug output.
85impl fmt::Debug for FlowError {
86    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Self::RunNotFound(run_id) => {
89                formatter.debug_tuple("RunNotFound").field(run_id).finish()
90            }
91            Self::RunTerminal(run_id) => {
92                formatter.debug_tuple("RunTerminal").field(run_id).finish()
93            }
94            Self::InvalidRunId(run_id) => {
95                formatter.debug_tuple("InvalidRunId").field(run_id).finish()
96            }
97            Self::RunConflict { run_id, reason } => formatter
98                .debug_struct("RunConflict")
99                .field("run_id", run_id)
100                .field("reason", reason)
101                .finish(),
102            Self::NonDeterministic { run_id, reason } => formatter
103                .debug_struct("NonDeterministic")
104                .field("run_id", run_id)
105                .field("reason", reason)
106                .finish(),
107            Self::EventConflict {
108                run_id,
109                expected_sequence,
110                actual_sequence,
111            } => formatter
112                .debug_struct("EventConflict")
113                .field("run_id", run_id)
114                .field("expected_sequence", expected_sequence)
115                .field("actual_sequence", actual_sequence)
116                .finish(),
117            Self::HookTokenNotFound(_) => formatter
118                .debug_tuple("HookTokenNotFound")
119                .field(&"<redacted>")
120                .finish(),
121            Self::LeaseLost(lease_id) => {
122                formatter.debug_tuple("LeaseLost").field(lease_id).finish()
123            }
124            Self::HookTokenConflict {
125                existing_run_id,
126                existing_hook_id,
127                ..
128            } => formatter
129                .debug_struct("HookTokenConflict")
130                .field("token", &"<redacted>")
131                .field("existing_run_id", existing_run_id)
132                .field("existing_hook_id", existing_hook_id)
133                .finish(),
134            Self::InvalidWorkflow(message) => formatter
135                .debug_tuple("InvalidWorkflow")
136                .field(message)
137                .finish(),
138            Self::InvalidTransition(message) => formatter
139                .debug_tuple("InvalidTransition")
140                .field(message)
141                .finish(),
142            Self::InvalidWorkerConfiguration(message) => formatter
143                .debug_tuple("InvalidWorkerConfiguration")
144                .field(message)
145                .finish(),
146            Self::TaskManagement(message) => formatter
147                .debug_tuple("TaskManagement")
148                .field(message)
149                .finish(),
150            Self::Store(message) => formatter.debug_tuple("Store").field(message).finish(),
151            Self::Runtime(message) => formatter.debug_tuple("Runtime").field(message).finish(),
152            Self::Serialization(error) => {
153                formatter.debug_tuple("Serialization").field(error).finish()
154            }
155            Self::Io(error) => formatter.debug_tuple("Io").field(error).finish(),
156            Self::ReplayLimitExceeded(limit) => formatter
157                .debug_tuple("ReplayLimitExceeded")
158                .field(limit)
159                .finish(),
160        }
161    }
162}