1use std::borrow::Cow;
4use std::net::SocketAddr;
5use std::path::PathBuf;
6
7use aion::EngineError;
8use aion_proto::WireError;
9use aion_store::StoreError;
10use thiserror::Error;
11
12#[path = "error_engine.rs"]
13mod engine;
14#[path = "error_process_exit.rs"]
15mod process_exit;
16
17#[derive(Debug, Error)]
19pub enum ServerError {
20 #[error("configuration error: {message}")]
22 Config {
23 message: String,
25 },
26
27 #[error(
29 "unsafe store.data_dir `{}`: ancestor `{}` is not owner-controlled: {reason}; \
30 move store.data_dir beneath the private Aion home (`$AION_HOME`, default `~/.aion`) \
31 and keep its ancestor chain owner-only",
32 .data_root.display(),
33 .component.display()
34 )]
35 UnsafeDataRootAncestor {
36 data_root: PathBuf,
38 component: PathBuf,
40 reason: String,
42 },
43
44 #[error("{transport} transport failed at {address}: {message}")]
46 TransportBind {
47 transport: &'static str,
49 address: SocketAddr,
51 message: String,
53 },
54
55 #[error("{transport} transport task failed: {message}")]
57 Transport {
58 transport: &'static str,
60 message: String,
62 },
63
64 #[error("{listener} listener failed: {message}")]
66 SignalListener {
67 listener: &'static str,
69 message: String,
71 },
72
73 #[error("namespace error: {message}")]
75 Namespace {
76 message: String,
78 },
79
80 #[error("engine call failed: {source}")]
82 EngineCall {
83 #[from]
85 source: EngineError,
86 },
87
88 #[error("store backend failed: {source}")]
90 StoreBackend {
91 #[from]
93 source: StoreError,
94 },
95
96 #[error("stream failure: {failure}")]
98 Stream {
99 failure: StreamFailure,
101 },
102
103 #[error(
105 "worker dispatch failed for namespace {namespace}, activity type {activity_type}: {reason}"
106 )]
107 WorkerDispatch {
108 namespace: String,
110 activity_type: String,
112 reason: String,
114 },
115
116 #[error("worker connection lost during dispatch on {channel}: {detail}")]
129 WorkerConnectionLost {
130 channel: String,
132 detail: String,
134 },
135
136 #[error("{resource} lock was poisoned")]
138 LockPoisoned {
139 resource: &'static str,
141 },
142
143 #[error("wire error: {wire}")]
145 Wire {
146 wire: WireError,
148 },
149}
150
151#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
153pub enum StreamFailure {
154 #[error("consumer lagged behind bounded buffer")]
156 Lagged,
157 #[error("subscriber connection closed")]
159 Closed,
160 #[error("engine event stream closed")]
162 UpstreamClosed,
163}
164
165impl From<WireError> for ServerError {
166 fn from(wire: WireError) -> Self {
167 Self::Wire { wire }
168 }
169}
170
171impl ServerError {
172 #[must_use]
175 pub fn to_wire_error(&self) -> WireError {
176 match self {
177 Self::Config { .. }
178 | Self::UnsafeDataRootAncestor { .. }
179 | Self::TransportBind { .. }
180 | Self::Transport { .. }
181 | Self::SignalListener { .. }
182 | Self::LockPoisoned { .. } => WireError::backend("server backend failure"),
183 Self::WorkerDispatch { .. } => WireError::backend("worker dispatch failed"),
184 Self::WorkerConnectionLost { .. } => {
185 WireError::backend("worker connection lost during dispatch")
186 }
187 Self::Namespace { message } => WireError::namespace_denied(message.clone()),
188 Self::EngineCall { source } => wire_from_engine(source),
189 Self::StoreBackend { source } => wire_from_store(source),
190 Self::Stream { failure } => match failure {
191 StreamFailure::Lagged => WireError::lagged("subscriber lagged behind"),
192 StreamFailure::Closed | StreamFailure::UpstreamClosed => {
193 WireError::backend("event stream closed")
194 }
195 },
196 Self::Wire { wire } => wire.clone(),
197 }
198 }
199
200 #[must_use]
202 pub const fn is_config(&self) -> bool {
203 matches!(
204 self,
205 Self::Config { .. } | Self::UnsafeDataRootAncestor { .. }
206 )
207 }
208
209 #[must_use]
211 pub fn namespace_denied(message: impl Into<String>) -> Self {
212 Self::Namespace {
213 message: message.into(),
214 }
215 }
216
217 #[must_use]
225 pub fn placement_admission_denied(
226 namespace: &str,
227 worker_node: Option<&str>,
228 required: &std::collections::BTreeSet<String>,
229 ) -> Self {
230 let node = worker_node.unwrap_or("none");
231 let required = required
232 .iter()
233 .map(String::as_str)
234 .collect::<Vec<_>>()
235 .join(", ");
236 Self::namespace_denied(format!(
237 "worker registration rejected: namespace {namespace} is Pinned to node label(s) \
238 [{required}] but the worker advertises node {node}, which is not in the required set"
239 ))
240 }
241
242 #[must_use]
245 pub fn deploy_denied(message: impl Into<String>) -> Self {
246 Self::Wire {
247 wire: WireError::deploy_denied(message),
248 }
249 }
250
251 #[must_use]
253 pub const fn lagged_stream() -> Self {
254 Self::Stream {
255 failure: StreamFailure::Lagged,
256 }
257 }
258
259 #[must_use]
261 pub fn worker_dispatch(
262 namespace: impl Into<String>,
263 activity_type: impl Into<String>,
264 reason: impl Into<String>,
265 ) -> Self {
266 Self::WorkerDispatch {
267 namespace: namespace.into(),
268 activity_type: activity_type.into(),
269 reason: reason.into(),
270 }
271 }
272
273 #[must_use]
276 pub fn worker_connection_lost(channel: impl Into<String>, detail: impl Into<String>) -> Self {
277 Self::WorkerConnectionLost {
278 channel: channel.into(),
279 detail: detail.into(),
280 }
281 }
282
283 #[must_use]
289 pub const fn is_worker_connection_lost(&self) -> bool {
290 matches!(self, Self::WorkerConnectionLost { .. })
291 }
292
293 #[must_use]
295 pub const fn lock_poisoned(resource: &'static str) -> Self {
296 Self::LockPoisoned { resource }
297 }
298}
299
300#[derive(Clone)]
302pub struct ErrorTraceFields<'a> {
303 pub error_type: Cow<'a, str>,
305 pub store_error_type: Option<&'static str>,
307 pub reason: &'a dyn std::fmt::Display,
309}
310
311impl ServerError {
312 #[must_use]
314 pub fn trace_fields(&self) -> ErrorTraceFields<'_> {
315 match self {
316 Self::Config { message } => ErrorTraceFields {
317 error_type: Cow::Borrowed("Config"),
318 store_error_type: None,
319 reason: message,
320 },
321 Self::UnsafeDataRootAncestor { reason, .. } => ErrorTraceFields {
322 error_type: Cow::Borrowed("UnsafeDataRootAncestor"),
323 store_error_type: None,
324 reason,
325 },
326 Self::TransportBind { message, .. } => ErrorTraceFields {
327 error_type: Cow::Borrowed("TransportBind"),
328 store_error_type: None,
329 reason: message,
330 },
331 Self::Transport { message, .. } => ErrorTraceFields {
332 error_type: Cow::Borrowed("Transport"),
333 store_error_type: None,
334 reason: message,
335 },
336 Self::SignalListener { message, .. } => ErrorTraceFields {
337 error_type: Cow::Borrowed("SignalListener"),
338 store_error_type: None,
339 reason: message,
340 },
341 Self::Namespace { message } => ErrorTraceFields {
342 error_type: Cow::Borrowed("Namespace"),
343 store_error_type: None,
344 reason: message,
345 },
346 Self::EngineCall { source } => engine_trace_fields(source),
347 Self::StoreBackend { source } => store_trace_fields(source),
348 Self::Stream { failure } => ErrorTraceFields {
349 error_type: Cow::Borrowed("Stream"),
350 store_error_type: None,
351 reason: failure,
352 },
353 Self::WorkerDispatch { reason, .. } => ErrorTraceFields {
354 error_type: Cow::Borrowed("WorkerDispatch"),
355 store_error_type: None,
356 reason,
357 },
358 Self::WorkerConnectionLost { detail, .. } => ErrorTraceFields {
359 error_type: Cow::Borrowed("WorkerConnectionLost"),
360 store_error_type: None,
361 reason: detail,
362 },
363 Self::LockPoisoned { resource } => ErrorTraceFields {
364 error_type: Cow::Borrowed("LockPoisoned"),
365 store_error_type: None,
366 reason: resource,
367 },
368 Self::Wire { wire } => ErrorTraceFields {
369 error_type: wire
370 .error_type
371 .as_deref()
372 .map_or_else(|| Cow::Borrowed(wire.code.as_str()), Cow::Borrowed),
373 store_error_type: None,
374 reason: wire,
375 },
376 }
377 }
378}
379
380fn engine_trace_fields(source: &EngineError) -> ErrorTraceFields<'_> {
381 match source {
382 EngineError::WorkflowNotFound { .. } => simple_engine_fields("WorkflowNotFound", source),
383 EngineError::InvalidState { .. } => simple_engine_fields("InvalidState", source),
384 EngineError::ScheduleNotFound { .. } => simple_engine_fields("ScheduleNotFound", source),
385 EngineError::ShuttingDown => simple_engine_fields("ShuttingDown", source),
386 EngineError::Store(store) => store_trace_fields(store),
387 EngineError::Durability(durability) => match durability {
388 aion::durability::DurabilityError::Store(store) => store_trace_fields(store),
389 aion::durability::DurabilityError::NonDeterminism(_)
390 | aion::durability::DurabilityError::HistoryShape { .. }
391 | aion::durability::DurabilityError::SearchAttribute(_) => {
392 simple_engine_fields("Durability", source)
393 }
394 },
395 EngineError::MissingStore => simple_engine_fields("MissingStore", source),
396 EngineError::MissingVisibilityStore => {
397 simple_engine_fields("MissingVisibilityStore", source)
398 }
399 EngineError::ConflictingEventPublisher => {
400 simple_engine_fields("ConflictingEventPublisher", source)
401 }
402 EngineError::EventStreaming(_) => simple_engine_fields("EventStreaming", source),
403 EngineError::Load { .. } => simple_engine_fields("Load", source),
404 EngineError::UnknownVersion { .. } => simple_engine_fields("UnknownVersion", source),
405 EngineError::VersionPinned { .. } => simple_engine_fields("VersionPinned", source),
406 EngineError::RouteActive { .. } => simple_engine_fields("RouteActive", source),
407 EngineError::ManifestMismatch { .. } => simple_engine_fields("ManifestMismatch", source),
408 EngineError::Package(_) => simple_engine_fields("Package", source),
409 EngineError::Schedule { .. } => simple_engine_fields("Schedule", source),
410 EngineError::Runtime { .. } => simple_engine_fields("Runtime", source),
411 EngineError::Gate3BifReplacementMissing { .. } => {
412 simple_engine_fields("Gate3BifReplacementMissing", source)
413 }
414 EngineError::CleanupExecutorPoisoned => {
415 simple_engine_fields("CleanupExecutorPoisoned", source)
416 }
417 EngineError::CleanupExecutorShutdownTimedOut { .. } => {
418 simple_engine_fields("CleanupExecutorShutdownTimedOut", source)
419 }
420 EngineError::ProcessExitRegistryPoisoned => {
421 simple_engine_fields("ProcessExitRegistryPoisoned", source)
422 }
423 EngineError::ProcessExitOwnershipPoisoned { .. } => {
424 simple_engine_fields("ProcessExitOwnershipPoisoned", source)
425 }
426 EngineError::ProcessExitStatePoisoned { .. } => {
427 process_exit::trace("ProcessExitStatePoisoned", source)
428 }
429 EngineError::ProcessExitSubscriptionUnavailable => {
430 process_exit::trace("ProcessExitSubscriptionUnavailable", source)
431 }
432 EngineError::ProcessExitDrainerSpawn { .. } => {
433 process_exit::trace("ProcessExitDrainerSpawn", source)
434 }
435 EngineError::ProcessExitDrainerPoisoned => {
436 process_exit::trace("ProcessExitDrainerPoisoned", source)
437 }
438 EngineError::ProcessExitOutcomeMissingAfterEvent { .. } => {
439 process_exit::trace("ProcessExitOutcomeMissingAfterEvent", source)
440 }
441 EngineError::ProcessExitEventStreamDisconnected => {
442 process_exit::trace("ProcessExitEventStreamDisconnected", source)
443 }
444 EngineError::ProcessExitDrainerShutdownTimedOut { .. } => {
445 process_exit::trace("ProcessExitDrainerShutdownTimedOut", source)
446 }
447 EngineError::ProcessExitDrainerPanicked => {
448 process_exit::trace("ProcessExitDrainerPanicked", source)
449 }
450 EngineError::ProcessExitCallbackDispatcherPoisoned
451 | EngineError::ProcessExitCallbackDispatcherUnavailable
452 | EngineError::ProcessExitCallbackDispatcherShutdownTimedOut { .. } => {
453 process_exit::callback_trace(source)
454 }
455 EngineError::ProcessExitAlreadyTerminal { .. } => {
456 simple_engine_fields("ProcessExitAlreadyTerminal", source)
457 }
458 EngineError::ActivityDeliveryPoisoned { .. } => {
459 simple_engine_fields("ActivityDeliveryPoisoned", source)
460 }
461 EngineError::RegistryPoisoned => simple_engine_fields("RegistryPoisoned", source),
462 EngineError::CatalogPoisoned => simple_engine_fields("CatalogPoisoned", source),
463 EngineError::NifRegistration { .. } => simple_engine_fields("NifRegistration", source),
464 EngineError::SignalRouter(_) => simple_engine_fields("SignalRouter", source),
465 EngineError::Query(query) => simple_engine_fields(engine::query_error_type(query), source),
466 }
467}
468
469fn simple_engine_fields<'a>(
470 error_type: &'static str,
471 source: &'a EngineError,
472) -> ErrorTraceFields<'a> {
473 ErrorTraceFields {
474 error_type: Cow::Borrowed(error_type),
475 store_error_type: None,
476 reason: source,
477 }
478}
479
480fn store_trace_fields(source: &StoreError) -> ErrorTraceFields<'_> {
481 ErrorTraceFields {
482 error_type: Cow::Borrowed("StoreError"),
483 store_error_type: Some(store_error_type(source)),
484 reason: source,
485 }
486}
487
488fn store_error_type(source: &StoreError) -> &'static str {
489 match source {
490 StoreError::SequenceConflict { .. } => "SequenceConflict",
491 StoreError::NotFound { .. } => "NotFound",
492 StoreError::NotOwner { .. } => "NotOwner",
493 StoreError::Backend(_) => "Backend",
494 StoreError::Serialization(_) => "Serialization",
495 }
496}
497
498fn wire_from_engine(source: &EngineError) -> WireError {
499 use EngineError as E;
500 use engine::backend_wire as backend;
501
502 match source {
503 EngineError::WorkflowNotFound { .. } => {
504 WireError::not_found_with_type("WorkflowNotFound", source.to_string())
505 }
506 EngineError::InvalidState { reason } => engine::invalid_state_wire(reason),
510 EngineError::ScheduleNotFound { .. } => {
511 WireError::not_found_with_type("ScheduleNotFound", source.to_string())
512 }
513 EngineError::ShuttingDown => {
514 WireError::not_running_with_type("ShuttingDown", source.to_string())
515 }
516 EngineError::Store(store) => wire_from_store(store),
517 EngineError::Durability(durability) => engine::durability_wire(durability, source),
518 EngineError::MissingStore => engine::backend_wire("MissingStore", source),
519 E::MissingVisibilityStore => backend("MissingVisibilityStore", source),
520 E::ConflictingEventPublisher => backend("ConflictingEventPublisher", source),
521 EngineError::EventStreaming(_) => engine::backend_wire("EventStreaming", source),
522 EngineError::Load { .. } => WireError::backend_with_type("Load", source.to_string()),
523 EngineError::UnknownVersion { .. } => {
528 WireError::not_found_with_type("UnknownVersion", source.to_string())
529 }
530 EngineError::VersionPinned { .. } => {
531 WireError::version_pinned(source.to_string()).with_error_type("VersionPinned")
532 }
533 EngineError::RouteActive { .. } => {
534 WireError::version_pinned(source.to_string()).with_error_type("RouteActive")
535 }
536 EngineError::ManifestMismatch { .. } => {
537 WireError::invalid_input(source.to_string()).with_error_type("ManifestMismatch")
538 }
539 EngineError::Package(_) => WireError::backend_with_type("Package", source.to_string()),
540 EngineError::Schedule { .. } => {
541 WireError::backend_with_type("Schedule", source.to_string())
542 }
543 EngineError::Runtime { .. } => WireError::backend_with_type("Runtime", source.to_string()),
544 E::Gate3BifReplacementMissing { .. } => backend("Gate3BifReplacementMissing", source),
545 EngineError::CleanupExecutorPoisoned => {
546 WireError::backend_with_type("CleanupExecutorPoisoned", source.to_string())
547 }
548 EngineError::CleanupExecutorShutdownTimedOut { .. } => {
549 WireError::backend_with_type("CleanupExecutorShutdownTimedOut", source.to_string())
550 }
551 EngineError::ProcessExitRegistryPoisoned => {
552 WireError::backend_with_type("ProcessExitRegistryPoisoned", source.to_string())
553 }
554 EngineError::ProcessExitOwnershipPoisoned { .. } => {
555 WireError::backend_with_type("ProcessExitOwnershipPoisoned", source.to_string())
556 }
557 EngineError::ProcessExitStatePoisoned { .. } => {
558 process_exit::wire("ProcessExitStatePoisoned", source)
559 }
560 EngineError::ProcessExitSubscriptionUnavailable => {
561 process_exit::wire("ProcessExitSubscriptionUnavailable", source)
562 }
563 EngineError::ProcessExitDrainerSpawn { .. } => {
564 process_exit::wire("ProcessExitDrainerSpawn", source)
565 }
566 EngineError::ProcessExitDrainerPoisoned => {
567 process_exit::wire("ProcessExitDrainerPoisoned", source)
568 }
569 EngineError::ProcessExitOutcomeMissingAfterEvent { .. } => {
570 process_exit::wire("ProcessExitOutcomeMissingAfterEvent", source)
571 }
572 EngineError::ProcessExitEventStreamDisconnected => {
573 process_exit::wire("ProcessExitEventStreamDisconnected", source)
574 }
575 EngineError::ProcessExitDrainerShutdownTimedOut { .. } => {
576 process_exit::wire("ProcessExitDrainerShutdownTimedOut", source)
577 }
578 EngineError::ProcessExitDrainerPanicked => {
579 process_exit::wire("ProcessExitDrainerPanicked", source)
580 }
581 EngineError::ProcessExitCallbackDispatcherPoisoned
582 | EngineError::ProcessExitCallbackDispatcherUnavailable
583 | EngineError::ProcessExitCallbackDispatcherShutdownTimedOut { .. } => {
584 process_exit::callback_wire(source)
585 }
586 EngineError::ProcessExitAlreadyTerminal { .. } => {
587 WireError::backend_with_type("ProcessExitAlreadyTerminal", source.to_string())
588 }
589 EngineError::ActivityDeliveryPoisoned { .. } => {
590 WireError::backend_with_type("ActivityDeliveryPoisoned", source.to_string())
591 }
592 EngineError::CatalogPoisoned => {
593 WireError::backend_with_type("CatalogPoisoned", source.to_string())
594 }
595 EngineError::RegistryPoisoned => {
596 WireError::backend_with_type("RegistryPoisoned", source.to_string())
597 }
598 EngineError::NifRegistration { .. } => {
599 WireError::backend_with_type("NifRegistration", source.to_string())
600 }
601 EngineError::SignalRouter(_) => {
602 WireError::backend_with_type("SignalRouter", source.to_string())
603 }
604 EngineError::Query(query) => engine::query_wire(query, source),
605 }
606}
607
608fn wire_from_store(source: &StoreError) -> WireError {
609 match source {
610 StoreError::SequenceConflict { .. } => WireError::new_with_type(
611 aion_proto::WireErrorCode::SequenceConflict,
612 "SequenceConflict",
613 source.to_string(),
614 ),
615 StoreError::NotFound { .. } => {
616 WireError::not_found_with_type("NotFound", source.to_string())
617 }
618 StoreError::NotOwner { .. } => {
619 WireError::not_owner(source.to_string()).with_error_type("NotOwner")
620 }
621 StoreError::Backend(_) => WireError::backend_with_type("Backend", source.to_string()),
622 StoreError::Serialization(_) => {
623 WireError::backend_with_type("Serialization", source.to_string())
624 }
625 }
626}
627
628#[cfg(test)]
629#[path = "error_tests.rs"]
630mod tests;