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