Skip to main content

harmos_runtime/
error.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::convert::Infallible;
18// Third Party Imports
19
20// Local Imports
21
22/* === Constants === */
23
24/* === Macros === */
25
26/* === Type Aliases === */
27
28/* === Structs === */
29
30/* === Enums === */
31
32#[derive(Debug, thiserror::Error)]
33pub enum Error<E = Infallible> {
34    #[error(transparent)]
35    Dependency(#[from] harmos_sdk::ResourceError),
36
37    #[error("runtime shutdown budget expired during {phase}; cleanup remains owned")]
38    ShutdownTimeout { phase: &'static str },
39
40    #[error("application stop failed ({stop}); cleanup also failed: {cleanup}")]
41    ApplicationLifecycle {
42        stop: std::sync::Arc<Error>,
43
44        #[source]
45        cleanup: std::sync::Arc<Error>,
46    },
47
48    #[error("authenticated caller identity is required")]
49    Unauthenticated,
50
51    #[error("caller is not permitted to perform this action")]
52    PermissionDenied,
53
54    #[error("record protobuf schema is invalid: {source}")]
55    RecordSchema {
56        #[source]
57        source: crate::schema::Error,
58    },
59
60    #[error("record protobuf payload is invalid: {source}")]
61    RecordPayload {
62        #[source]
63        source: crate::schema::PayloadError,
64    },
65
66    #[error("record declaration is unavailable: {source}")]
67    RecordDeclaration {
68        #[source]
69        source: crate::state::DecodeError,
70    },
71
72    #[error(transparent)]
73    OperationRefused(#[from] crate::Refusal),
74
75    #[error("operation child lane `{child}` would cycle with held lane `{parent}`")]
76    OperationLaneCycle { parent: &'static str, child: &'static str },
77
78    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
79    #[error("operation caller is unavailable: {source}")]
80    OperationCaller {
81        #[source]
82        source: crate::plugin::DispatchError,
83    },
84
85    #[error("caller `{caller}` may not invoke operation `{operation}` on `{owner}`")]
86    OperationUnauthorized { caller: String, owner: String, operation: String },
87
88    #[error("operation `{operation}` payload is unreadable: {source}")]
89    OperationPayload {
90        operation: String,
91
92        #[source]
93        source: harmos_domain::MessageError,
94    },
95
96    #[error("operation `{operation}` panicked")]
97    OperationPanicked { operation: String },
98
99    #[error("operation declaration is invalid: {source}")]
100    OperationDeclaration {
101        #[source]
102        source: harmos_domain::Error,
103    },
104
105    // Prints its own line; the cause is the source, read through `failure()`.
106    #[error("operation `{operation}` failed")]
107    OperationFailed {
108        operation: String,
109
110        #[source]
111        source: std::sync::Arc<dyn std::error::Error + Send + Sync>,
112    },
113
114    #[error("operation `{operation}` could not finalize its invocation state")]
115    OperationFinalization {
116        operation: String,
117        outcome: Option<std::sync::Arc<Error>>,
118
119        #[source]
120        source: std::sync::Arc<Error>,
121    },
122
123    #[error("operation physical settlement failed: {source}")]
124    OperationSettlement {
125        #[source]
126        source: std::sync::Arc<Error>,
127    },
128
129    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
130    #[error("plugin lifecycle failed: {source}")]
131    PluginLifecycle {
132        #[source]
133        source: Box<crate::plugin::PluginFailure>,
134    },
135
136    #[error("{domain} task failed: {source}")]
137    TaskFailed {
138        domain: &'static str,
139
140        #[source]
141        source: std::sync::Arc<dyn std::error::Error + Send + Sync>,
142    },
143
144    #[error("{domain} is not ready")]
145    NotReady { domain: &'static str },
146
147    #[error("persistence failed: {source}")]
148    Persistence {
149        #[source]
150        source: std::sync::Arc<Error>,
151    },
152
153    #[error("{domain} admission is closed")]
154    Closing { domain: &'static str },
155
156    #[error("{domain} `{name}` is already registered")]
157    AlreadyRegistered { domain: &'static str, name: String },
158
159    #[error("{domain} `{name}` is not registered")]
160    NotRegistered { domain: &'static str, name: String },
161
162    #[error("backend {operation} failed ({kind:?}): {source}")]
163    Backend {
164        operation: &'static str,
165        kind: BackendFailure,
166
167        #[source]
168        source: Box<dyn std::error::Error + Send + Sync>,
169    },
170
171    #[error("invalid {field}: {reason}")]
172    InvalidInput { field: &'static str, reason: &'static str },
173
174    #[error("{resource} capacity is exhausted")]
175    ResourceExhausted { resource: &'static str },
176
177    #[error("positions belong to different store histories")]
178    DifferentHistory,
179
180    #[error("the read view has expired")]
181    ViewExpired,
182
183    #[error("retained data begins at {available}, but {requested} was requested")]
184    Gap { requested: u64, available: u64 },
185
186    #[error("required journal capture failed")]
187    CaptureFailed,
188
189    #[error("the requested state boundary splits a logical commit")]
190    IncompleteCommit,
191
192    #[error("the logical commit could not be prepared: {0}")]
193    Preparation(PreparationError),
194
195    #[error("the transaction was refused: {0}")]
196    Rejected(E),
197
198    #[error("the undo conflicts with the current state: {0}")]
199    UndoConflict(E),
200
201    #[error("the redo conflicts with the current state: {0}")]
202    RedoConflict(E),
203
204    #[error("this principal has nothing to undo in scope")]
205    NothingToUndo,
206
207    #[error("this principal has nothing to redo in scope")]
208    NothingToRedo,
209
210    #[error("the supplied origin is above the requested position")]
211    OriginAhead,
212
213    #[error("the requested query frontier is above the applied position")]
214    FrontierAhead,
215
216    #[error("the requested position is beneath the retained-history horizon")]
217    BeneathHorizon,
218
219    #[error("the requested position is below the in-memory tail's floor")]
220    BeyondTail,
221
222    #[error("the journal is not admitting requests")]
223    Unavailable,
224}
225
226pub use harmos_storage::BackendFailure;
227
228pub use harmos_domain::state::PreparationError;
229
230/* === Traits === */
231
232/* === Implementations === */
233
234impl<E> From<harmos_storage::Error> for Error<E> {
235    fn from(error: harmos_storage::Error) -> Self {
236        use harmos_storage::Error as Storage;
237        match error {
238            Storage::OperationFailed { operation, source } => Self::OperationFailed { operation, source },
239            Storage::NotReady { domain } => Self::NotReady { domain },
240            Storage::Persistence { source } => Self::Persistence {
241                source: std::sync::Arc::new(std::sync::Arc::try_unwrap(source).map(Error::from).unwrap_or_else(|source| {
242                    Error::backend("storage persistence", source.backend_failure().unwrap_or(BackendFailure::Permanent), source)
243                })),
244            },
245            Storage::Closing { domain } => Self::Closing { domain },
246            Storage::AlreadyRegistered { domain, name } => Self::AlreadyRegistered { domain, name },
247            Storage::Backend { operation, kind, source } => Self::Backend { operation, kind, source },
248            Storage::InvalidInput { field, reason } => Self::InvalidInput { field, reason },
249            Storage::ResourceExhausted { resource } => Self::ResourceExhausted { resource },
250            Storage::Gap { requested, available } => Self::Gap { requested, available },
251            Storage::Domain(source) => source.into(),
252        }
253    }
254}
255
256impl<E> From<harmos_domain::Error> for Error<E> {
257    fn from(error: harmos_domain::Error) -> Self {
258        match error {
259            harmos_domain::Error::InvalidInput { field, reason } => Self::InvalidInput { field, reason },
260            harmos_domain::Error::ResourceExhausted { resource } => Self::ResourceExhausted { resource },
261            harmos_domain::Error::DifferentHistory => Self::DifferentHistory,
262            harmos_domain::Error::Corrupt { operation, source } => Self::backend(operation, BackendFailure::Corrupt, source),
263            source => Self::OperationDeclaration { source },
264        }
265    }
266}
267
268impl<E> From<harmos_domain::operations::ContextError> for Error<E> {
269    fn from(error: harmos_domain::operations::ContextError) -> Self {
270        use harmos_domain::operations::ContextError;
271        match error {
272            ContextError::Duplicate { name } => Self::AlreadyRegistered { domain: "call context", name },
273            ContextError::Missing { name } => Self::NotRegistered { domain: "call context", name },
274            ContextError::Decode { .. } => Self::InvalidInput {
275                field: "call context",
276                reason: "portable value is unreadable",
277            },
278            ContextError::Budget => Self::InvalidInput {
279                field: "call context",
280                reason: "portable values exceed their budget",
281            },
282        }
283    }
284}
285
286impl<E> Error<E> {
287    // The structured failure this error carries from a plugin, with its cause
288    // written once at the origin and the operation frame the host added.
289    pub fn failure(&self) -> Option<&harmos_domain::Failure> {
290        match self {
291            Self::ApplicationLifecycle { stop, cleanup } => stop.failure().or_else(|| cleanup.failure()),
292            Self::OperationFailed { source, .. } | Self::TaskFailed { source, .. } => carried(source.as_ref()),
293            // The Attempt's Outcome Names The Failure; Cleanup Is Secondary
294            Self::OperationFinalization { outcome, source, .. } => outcome.as_deref().and_then(Error::failure).or_else(|| source.failure()),
295            Self::OperationSettlement { source } => source.failure(),
296            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
297            Self::PluginLifecycle { source } => carried(source.as_ref()),
298            _ => None,
299        }
300    }
301
302    pub fn backend(operation: &'static str, kind: BackendFailure, source: impl std::error::Error + Send + Sync + 'static) -> Self {
303        Self::Backend {
304            operation,
305            kind,
306            source: Box::new(source),
307        }
308    }
309}
310
311/* === Public Functions === */
312
313/* === Private Functions === */
314
315// The first failure along a source chain, read through the carrier that
316// transported it where that carrier hides it behind a transparent line.
317fn carried<'a>(source: &'a (dyn std::error::Error + 'static)) -> Option<&'a harmos_domain::Failure> {
318    std::iter::successors(Some(source), |link| link.source()).find_map(|link| {
319        if let Some(error) = link.downcast_ref::<Error>() {
320            return error.failure();
321        }
322        #[cfg(feature = "plugin-sidecar")]
323        if let Some(error) = link.downcast_ref::<crate::operations::NativeError>() {
324            return error.failure();
325        }
326        #[cfg(all(feature = "plugin-guest", not(target_arch = "wasm32")))]
327        if let Some(error) = link.downcast_ref::<crate::operations::ComponentError>() {
328            return error.failure();
329        }
330        harmos_domain::Failure::find(link)
331    })
332}
333
334/* === Tests === */