1use std::convert::Infallible;
18#[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 #[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
230impl<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 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 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
311fn 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