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