Skip to main content

a3s_flow/
error.rs

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