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("hook {hook_id} for workflow run {run_id} conflicts with request: {reason}")]
55    HookConflict {
56        run_id: String,
57        hook_id: String,
58        reason: String,
59    },
60
61    #[error("invalid workflow definition: {0}")]
62    InvalidWorkflow(String),
63
64    #[error("invalid state transition: {0}")]
65    InvalidTransition(String),
66
67    #[error("invalid worker configuration: {0}")]
68    InvalidWorkerConfiguration(String),
69
70    #[error("task manager error: {0}")]
71    TaskManagement(String),
72
73    #[error("event store error: {0}")]
74    Store(String),
75
76    #[error("runtime error: {0}")]
77    Runtime(String),
78
79    #[error("serialization error: {0}")]
80    Serialization(#[from] serde_json::Error),
81
82    #[error("io error: {0}")]
83    Io(#[from] std::io::Error),
84
85    #[error("workflow replay exceeded {0} iterations")]
86    ReplayLimitExceeded(usize),
87}
88
89// Error values can retain callback tokens for programmatic recovery, but
90// diagnostics must never reveal those bearer credentials. Keep ordinary
91// variants structurally useful while replacing token fields in Debug output.
92impl fmt::Debug for FlowError {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Self::RunNotFound(run_id) => {
96                formatter.debug_tuple("RunNotFound").field(run_id).finish()
97            }
98            Self::RunTerminal(run_id) => {
99                formatter.debug_tuple("RunTerminal").field(run_id).finish()
100            }
101            Self::InvalidRunId(run_id) => {
102                formatter.debug_tuple("InvalidRunId").field(run_id).finish()
103            }
104            Self::RunConflict { run_id, reason } => formatter
105                .debug_struct("RunConflict")
106                .field("run_id", run_id)
107                .field("reason", reason)
108                .finish(),
109            Self::NonDeterministic { run_id, reason } => formatter
110                .debug_struct("NonDeterministic")
111                .field("run_id", run_id)
112                .field("reason", reason)
113                .finish(),
114            Self::EventConflict {
115                run_id,
116                expected_sequence,
117                actual_sequence,
118            } => formatter
119                .debug_struct("EventConflict")
120                .field("run_id", run_id)
121                .field("expected_sequence", expected_sequence)
122                .field("actual_sequence", actual_sequence)
123                .finish(),
124            Self::HookTokenNotFound(_) => formatter
125                .debug_tuple("HookTokenNotFound")
126                .field(&"<redacted>")
127                .finish(),
128            Self::LeaseLost(lease_id) => {
129                formatter.debug_tuple("LeaseLost").field(lease_id).finish()
130            }
131            Self::HookTokenConflict {
132                existing_run_id,
133                existing_hook_id,
134                ..
135            } => formatter
136                .debug_struct("HookTokenConflict")
137                .field("token", &"<redacted>")
138                .field("existing_run_id", existing_run_id)
139                .field("existing_hook_id", existing_hook_id)
140                .finish(),
141            Self::HookConflict {
142                run_id,
143                hook_id,
144                reason,
145            } => formatter
146                .debug_struct("HookConflict")
147                .field("run_id", run_id)
148                .field("hook_id", hook_id)
149                .field("reason", reason)
150                .finish(),
151            Self::InvalidWorkflow(message) => formatter
152                .debug_tuple("InvalidWorkflow")
153                .field(message)
154                .finish(),
155            Self::InvalidTransition(message) => formatter
156                .debug_tuple("InvalidTransition")
157                .field(message)
158                .finish(),
159            Self::InvalidWorkerConfiguration(message) => formatter
160                .debug_tuple("InvalidWorkerConfiguration")
161                .field(message)
162                .finish(),
163            Self::TaskManagement(message) => formatter
164                .debug_tuple("TaskManagement")
165                .field(message)
166                .finish(),
167            Self::Store(message) => formatter.debug_tuple("Store").field(message).finish(),
168            Self::Runtime(message) => formatter.debug_tuple("Runtime").field(message).finish(),
169            Self::Serialization(error) => {
170                formatter.debug_tuple("Serialization").field(error).finish()
171            }
172            Self::Io(error) => formatter.debug_tuple("Io").field(error).finish(),
173            Self::ReplayLimitExceeded(limit) => formatter
174                .debug_tuple("ReplayLimitExceeded")
175                .field(limit)
176                .finish(),
177        }
178    }
179}