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    /// 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}