aion/error.rs
1//! Engine error taxonomy.
2
3use crate::schedule::{ScheduleError, ScheduleEvaluatorError};
4use aion_core::{RunId, ScheduleId, WorkflowId};
5use aion_package::{ContentHash, PackageError};
6use aion_store::StoreError;
7
8use crate::durability::DurabilityError;
9
10/// Errors returned by the embedded workflow engine.
11#[derive(thiserror::Error, Debug)]
12pub enum EngineError {
13 /// The builder was asked to construct an engine without an event store.
14 #[error("engine store is required")]
15 MissingStore,
16
17 /// The builder was asked to construct an engine without a visibility store.
18 #[error(
19 "engine visibility store is required; call EngineBuilder::visibility_store() or EngineBuilder::in_memory_visibility()"
20 )]
21 MissingVisibilityStore,
22
23 /// A workflow package failed to load or validate for engine registration.
24 #[error("workflow package load failed: {reason}")]
25 Load {
26 /// Human-readable load failure reason.
27 reason: String,
28 },
29
30 /// A route or unload targeted a `(workflow type, version)` that is not loaded.
31 #[error(
32 "workflow `{workflow_type}` version `{version}` is not loaded (loaded versions: {loaded})"
33 )]
34 UnknownVersion {
35 /// Logical workflow type requested by the caller.
36 workflow_type: String,
37 /// Content-hash version requested by the caller.
38 version: ContentHash,
39 /// Comma-separated loaded versions of the type, or `none`.
40 loaded: String,
41 },
42
43 /// An unload was refused because something still pins the version.
44 #[error("cannot unload workflow `{workflow_type}` version `{version}`: {pinned_by}")]
45 VersionPinned {
46 /// Logical workflow type targeted by the unload.
47 workflow_type: String,
48 /// Content-hash version targeted by the unload.
49 version: ContentHash,
50 /// What pins the version, naming the concrete holder.
51 pinned_by: PinHolder,
52 },
53
54 /// An unload was refused because the version is route-active for its type.
55 #[error(
56 "cannot unload workflow `{workflow_type}` version `{version}`: it is the route-active version; route another version first"
57 )]
58 RouteActive {
59 /// Logical workflow type targeted by the unload.
60 workflow_type: String,
61 /// Content-hash version targeted by the unload.
62 version: ContentHash,
63 },
64
65 /// An idempotent re-load presented the resident content hash with a
66 /// different manifest. The content hash covers the canonical beam set
67 /// only, so this is the wrong-deploy tripwire: the resident version is
68 /// retained untouched and the incoming archive is refused.
69 #[error(
70 "workflow `{workflow_type}` version `{version}` is already loaded with a different manifest (resident digest {resident_digest}, incoming digest {incoming_digest}); the content hash covers beams only — rebuild the archive so its manifest matches the resident version, or change the beam set"
71 )]
72 ManifestMismatch {
73 /// Logical workflow type targeted by the load.
74 workflow_type: String,
75 /// Content-hash version shared by both archives.
76 version: ContentHash,
77 /// Canonical digest of the resident manifest.
78 resident_digest: String,
79 /// Canonical digest of the incoming manifest.
80 incoming_digest: String,
81 },
82
83 /// The builder was given both `event_streaming` and an explicit event-publisher seam.
84 #[error(
85 "conflicting event publisher configuration: EngineBuilder::event_streaming installs the broadcast publisher and cannot be combined with EngineBuilder::event_publisher"
86 )]
87 ConflictingEventPublisher,
88
89 /// Live event streaming setup failed.
90 #[error("event streaming setup failed: {0}")]
91 EventStreaming(#[from] crate::publish::PublishError),
92
93 /// The configured event store returned an error.
94 #[error("store error: {0}")]
95 Store(#[from] StoreError),
96
97 /// The durability recorder or replay path returned an error.
98 #[error("durability error: {0}")]
99 Durability(#[from] DurabilityError),
100
101 /// A `.aion` package operation returned an error.
102 #[error("package error: {0}")]
103 Package(#[from] PackageError),
104
105 /// The embedded runtime returned an error.
106 #[error("runtime error: {reason}")]
107 Runtime {
108 /// Human-readable runtime failure reason.
109 reason: String,
110 },
111
112 /// The active workflow registry lock was poisoned.
113 #[error("active workflow registry lock was poisoned")]
114 RegistryPoisoned,
115
116 /// The workflow catalog lock was poisoned.
117 #[error("workflow catalog lock was poisoned")]
118 CatalogPoisoned,
119
120 /// A precondition on the target workflow's current state was not met.
121 ///
122 /// Raised by the reopen operation when the target run is not in a reopenable
123 /// state: not terminal, terminal for a non-reopenable reason
124 /// (Completed/`TimedOut`), or already Running. The `reason` names the actual
125 /// status so callers and operators can see why the reopen was rejected. Maps
126 /// to the `INVALID_STATE` wire code (gRPC `FailedPrecondition` / HTTP 409).
127 #[error("invalid workflow state: {reason}")]
128 InvalidState {
129 /// Human-readable precondition-failure reason naming the actual status.
130 reason: String,
131 },
132
133 /// The engine is already shutting down and no new workflow starts are accepted.
134 #[error("engine is shutting down")]
135 ShuttingDown,
136
137 /// No live, durable, or loaded workflow was found for the request.
138 #[error("workflow `{workflow_type}` was not found")]
139 WorkflowNotFound {
140 /// Logical workflow type requested by the caller.
141 workflow_type: String,
142 },
143
144 /// No durable schedule was found for the request.
145 #[error("schedule `{schedule_id}` was not found")]
146 ScheduleNotFound {
147 /// Schedule identifier requested by the caller.
148 schedule_id: ScheduleId,
149 },
150
151 /// Schedule trigger, projection, or evaluator side effect failed.
152 #[error("schedule error: {reason}")]
153 Schedule {
154 /// Human-readable schedule failure reason.
155 reason: String,
156 },
157
158 /// Native implemented function registration failed.
159 #[error("NIF registration failed: {reason}")]
160 NifRegistration {
161 /// Human-readable native implemented function registration failure reason.
162 reason: String,
163 },
164
165 /// Signal routing failed after the target was resolved.
166 #[error("signal router error: {0}")]
167 SignalRouter(#[from] SignalRouterError),
168
169 /// Live workflow query dispatch failed after the target was resolved.
170 #[error("query error: {0}")]
171 Query(#[from] crate::query::QueryError),
172}
173
174/// What pins a workflow version against unload, naming the concrete holder.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum PinHolder {
177 /// A start resolved this version but has not yet registered a handle.
178 InFlightStart,
179 /// A live, non-terminal run executes on this version.
180 LiveRun {
181 /// Pinning workflow id.
182 workflow_id: WorkflowId,
183 /// Pinning run id.
184 run_id: RunId,
185 },
186 /// A recoverable instance in the store is pinned to this version.
187 RecoverableRun {
188 /// Pinning workflow id.
189 workflow_id: WorkflowId,
190 },
191 /// A recorded-but-never-started child is pinned to this version.
192 RecordedChild {
193 /// Child workflow id pinned to the version.
194 child_workflow_id: WorkflowId,
195 /// Parent workflow whose history records the child.
196 recorded_by: WorkflowId,
197 },
198}
199
200impl std::fmt::Display for PinHolder {
201 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 match self {
203 Self::InFlightStart => formatter.write_str("an in-flight start is pinned to it"),
204 Self::LiveRun {
205 workflow_id,
206 run_id,
207 } => write!(
208 formatter,
209 "live run `{workflow_id}/{run_id}` is pinned to it"
210 ),
211 Self::RecoverableRun { workflow_id } => {
212 write!(formatter, "recoverable run `{workflow_id}` is pinned to it")
213 }
214 Self::RecordedChild {
215 child_workflow_id,
216 recorded_by,
217 } => write!(
218 formatter,
219 "child `{child_workflow_id}` recorded by `{recorded_by}` is pinned to it and has not started"
220 ),
221 }
222 }
223}
224
225/// Errors surfaced by the signal routing boundary.
226#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
227pub enum SignalRouterError {
228 /// The target workflow is terminal and cannot receive new signals.
229 #[error("workflow {workflow_id}/{run_id} is terminal")]
230 Terminal {
231 /// Target workflow id.
232 workflow_id: WorkflowId,
233 /// Target run id.
234 run_id: RunId,
235 },
236
237 /// The router could not defer a recorded non-resident signal.
238 #[error("signal resume handoff failed: {reason}")]
239 Handoff {
240 /// Human-readable handoff failure reason.
241 reason: String,
242 },
243
244 /// The signal was durably recorded but could not be delivered to the live mailbox.
245 #[error(
246 "signal `{signal_name}` for workflow {workflow_id}/{run_id} could not be delivered to process {process_id}: {reason}"
247 )]
248 DeliveryFailed {
249 /// Target workflow id.
250 workflow_id: WorkflowId,
251 /// Target run id.
252 run_id: RunId,
253 /// Embedded runtime process identifier selected for delivery.
254 process_id: u64,
255 /// Signal name that was recorded and attempted.
256 signal_name: String,
257 /// Human-readable delivery failure reason.
258 reason: String,
259 },
260}
261
262impl From<ScheduleError> for EngineError {
263 fn from(error: ScheduleError) -> Self {
264 Self::Schedule {
265 reason: error.to_string(),
266 }
267 }
268}
269
270impl From<ScheduleEvaluatorError> for EngineError {
271 fn from(error: ScheduleEvaluatorError) -> Self {
272 match error {
273 ScheduleEvaluatorError::ScheduleNotFound { schedule_id } => {
274 Self::ScheduleNotFound { schedule_id }
275 }
276 other => Self::Schedule {
277 reason: other.to_string(),
278 },
279 }
280 }
281}