Skip to main content

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    /// The engine is already shutting down and no new workflow starts are accepted.
121    #[error("engine is shutting down")]
122    ShuttingDown,
123
124    /// No live, durable, or loaded workflow was found for the request.
125    #[error("workflow `{workflow_type}` was not found")]
126    WorkflowNotFound {
127        /// Logical workflow type requested by the caller.
128        workflow_type: String,
129    },
130
131    /// No durable schedule was found for the request.
132    #[error("schedule `{schedule_id}` was not found")]
133    ScheduleNotFound {
134        /// Schedule identifier requested by the caller.
135        schedule_id: ScheduleId,
136    },
137
138    /// Schedule trigger, projection, or evaluator side effect failed.
139    #[error("schedule error: {reason}")]
140    Schedule {
141        /// Human-readable schedule failure reason.
142        reason: String,
143    },
144
145    /// Native implemented function registration failed.
146    #[error("NIF registration failed: {reason}")]
147    NifRegistration {
148        /// Human-readable native implemented function registration failure reason.
149        reason: String,
150    },
151
152    /// Signal routing failed after the target was resolved.
153    #[error("signal router error: {0}")]
154    SignalRouter(#[from] SignalRouterError),
155
156    /// Live workflow query dispatch failed after the target was resolved.
157    #[error("query error: {0}")]
158    Query(#[from] crate::query::QueryError),
159}
160
161/// What pins a workflow version against unload, naming the concrete holder.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum PinHolder {
164    /// A start resolved this version but has not yet registered a handle.
165    InFlightStart,
166    /// A live, non-terminal run executes on this version.
167    LiveRun {
168        /// Pinning workflow id.
169        workflow_id: WorkflowId,
170        /// Pinning run id.
171        run_id: RunId,
172    },
173    /// A recoverable instance in the store is pinned to this version.
174    RecoverableRun {
175        /// Pinning workflow id.
176        workflow_id: WorkflowId,
177    },
178    /// A recorded-but-never-started child is pinned to this version.
179    RecordedChild {
180        /// Child workflow id pinned to the version.
181        child_workflow_id: WorkflowId,
182        /// Parent workflow whose history records the child.
183        recorded_by: WorkflowId,
184    },
185}
186
187impl std::fmt::Display for PinHolder {
188    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            Self::InFlightStart => formatter.write_str("an in-flight start is pinned to it"),
191            Self::LiveRun {
192                workflow_id,
193                run_id,
194            } => write!(
195                formatter,
196                "live run `{workflow_id}/{run_id}` is pinned to it"
197            ),
198            Self::RecoverableRun { workflow_id } => {
199                write!(formatter, "recoverable run `{workflow_id}` is pinned to it")
200            }
201            Self::RecordedChild {
202                child_workflow_id,
203                recorded_by,
204            } => write!(
205                formatter,
206                "child `{child_workflow_id}` recorded by `{recorded_by}` is pinned to it and has not started"
207            ),
208        }
209    }
210}
211
212/// Errors surfaced by the signal routing boundary.
213#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
214pub enum SignalRouterError {
215    /// The target workflow is terminal and cannot receive new signals.
216    #[error("workflow {workflow_id}/{run_id} is terminal")]
217    Terminal {
218        /// Target workflow id.
219        workflow_id: WorkflowId,
220        /// Target run id.
221        run_id: RunId,
222    },
223
224    /// The router could not defer a recorded non-resident signal.
225    #[error("signal resume handoff failed: {reason}")]
226    Handoff {
227        /// Human-readable handoff failure reason.
228        reason: String,
229    },
230
231    /// The signal was durably recorded but could not be delivered to the live mailbox.
232    #[error(
233        "signal `{signal_name}` for workflow {workflow_id}/{run_id} could not be delivered to process {process_id}: {reason}"
234    )]
235    DeliveryFailed {
236        /// Target workflow id.
237        workflow_id: WorkflowId,
238        /// Target run id.
239        run_id: RunId,
240        /// Embedded runtime process identifier selected for delivery.
241        process_id: u64,
242        /// Signal name that was recorded and attempted.
243        signal_name: String,
244        /// Human-readable delivery failure reason.
245        reason: String,
246    },
247}
248
249impl From<ScheduleError> for EngineError {
250    fn from(error: ScheduleError) -> Self {
251        Self::Schedule {
252            reason: error.to_string(),
253        }
254    }
255}
256
257impl From<ScheduleEvaluatorError> for EngineError {
258    fn from(error: ScheduleEvaluatorError) -> Self {
259        match error {
260            ScheduleEvaluatorError::ScheduleNotFound { schedule_id } => {
261                Self::ScheduleNotFound { schedule_id }
262            }
263            other => Self::Schedule {
264                reason: other.to_string(),
265            },
266        }
267    }
268}