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#[derive(Debug, Error)]
14pub enum ServerError {
15 #[error("configuration error: {message}")]
17 Config {
18 message: String,
20 },
21
22 #[error(
24 "unsafe store.data_dir `{}`: ancestor `{}` is not owner-controlled: {reason}; \
25 move store.data_dir beneath the private Aion home (`$AION_HOME`, default `~/.aion`) \
26 and keep its ancestor chain owner-only",
27 .data_root.display(),
28 .component.display()
29 )]
30 UnsafeDataRootAncestor {
31 data_root: PathBuf,
33 component: PathBuf,
35 reason: String,
37 },
38
39 #[error("{transport} transport failed at {address}: {message}")]
41 TransportBind {
42 transport: &'static str,
44 address: SocketAddr,
46 message: String,
48 },
49
50 #[error("{transport} transport task failed: {message}")]
52 Transport {
53 transport: &'static str,
55 message: String,
57 },
58
59 #[error("{listener} listener failed: {message}")]
61 SignalListener {
62 listener: &'static str,
64 message: String,
66 },
67
68 #[error("namespace error: {message}")]
70 Namespace {
71 message: String,
73 },
74
75 #[error("engine call failed: {source}")]
77 EngineCall {
78 #[from]
80 source: EngineError,
81 },
82
83 #[error("store backend failed: {source}")]
85 StoreBackend {
86 #[from]
88 source: StoreError,
89 },
90
91 #[error("stream failure: {failure}")]
93 Stream {
94 failure: StreamFailure,
96 },
97
98 #[error(
100 "worker dispatch failed for namespace {namespace}, activity type {activity_type}: {reason}"
101 )]
102 WorkerDispatch {
103 namespace: String,
105 activity_type: String,
107 reason: String,
109 },
110
111 #[error("worker connection lost during dispatch on {channel}: {detail}")]
124 WorkerConnectionLost {
125 channel: String,
127 detail: String,
129 },
130
131 #[error("{resource} lock was poisoned")]
133 LockPoisoned {
134 resource: &'static str,
136 },
137
138 #[error("wire error: {wire}")]
140 Wire {
141 wire: WireError,
143 },
144}
145
146#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
148pub enum StreamFailure {
149 #[error("consumer lagged behind bounded buffer")]
151 Lagged,
152 #[error("subscriber connection closed")]
154 Closed,
155 #[error("engine event stream closed")]
157 UpstreamClosed,
158}
159
160impl From<WireError> for ServerError {
161 fn from(wire: WireError) -> Self {
162 Self::Wire { wire }
163 }
164}
165
166impl ServerError {
167 #[must_use]
170 pub fn to_wire_error(&self) -> WireError {
171 match self {
172 Self::Config { .. }
173 | Self::UnsafeDataRootAncestor { .. }
174 | Self::TransportBind { .. }
175 | Self::Transport { .. }
176 | Self::SignalListener { .. }
177 | Self::LockPoisoned { .. } => WireError::backend("server backend failure"),
178 Self::WorkerDispatch { .. } => WireError::backend("worker dispatch failed"),
179 Self::WorkerConnectionLost { .. } => {
180 WireError::backend("worker connection lost during dispatch")
181 }
182 Self::Namespace { message } => WireError::namespace_denied(message.clone()),
183 Self::EngineCall { source } => wire_from_engine(source),
184 Self::StoreBackend { source } => wire_from_store(source),
185 Self::Stream { failure } => match failure {
186 StreamFailure::Lagged => WireError::lagged("subscriber lagged behind"),
187 StreamFailure::Closed | StreamFailure::UpstreamClosed => {
188 WireError::backend("event stream closed")
189 }
190 },
191 Self::Wire { wire } => wire.clone(),
192 }
193 }
194
195 #[must_use]
197 pub const fn is_config(&self) -> bool {
198 matches!(
199 self,
200 Self::Config { .. } | Self::UnsafeDataRootAncestor { .. }
201 )
202 }
203
204 #[must_use]
206 pub fn namespace_denied(message: impl Into<String>) -> Self {
207 Self::Namespace {
208 message: message.into(),
209 }
210 }
211
212 #[must_use]
220 pub fn placement_admission_denied(
221 namespace: &str,
222 worker_node: Option<&str>,
223 required: &std::collections::BTreeSet<String>,
224 ) -> Self {
225 let node = worker_node.unwrap_or("none");
226 let required = required
227 .iter()
228 .map(String::as_str)
229 .collect::<Vec<_>>()
230 .join(", ");
231 Self::namespace_denied(format!(
232 "worker registration rejected: namespace {namespace} is Pinned to node label(s) \
233 [{required}] but the worker advertises node {node}, which is not in the required set"
234 ))
235 }
236
237 #[must_use]
240 pub fn deploy_denied(message: impl Into<String>) -> Self {
241 Self::Wire {
242 wire: WireError::deploy_denied(message),
243 }
244 }
245
246 #[must_use]
248 pub const fn lagged_stream() -> Self {
249 Self::Stream {
250 failure: StreamFailure::Lagged,
251 }
252 }
253
254 #[must_use]
256 pub fn worker_dispatch(
257 namespace: impl Into<String>,
258 activity_type: impl Into<String>,
259 reason: impl Into<String>,
260 ) -> Self {
261 Self::WorkerDispatch {
262 namespace: namespace.into(),
263 activity_type: activity_type.into(),
264 reason: reason.into(),
265 }
266 }
267
268 #[must_use]
271 pub fn worker_connection_lost(channel: impl Into<String>, detail: impl Into<String>) -> Self {
272 Self::WorkerConnectionLost {
273 channel: channel.into(),
274 detail: detail.into(),
275 }
276 }
277
278 #[must_use]
284 pub const fn is_worker_connection_lost(&self) -> bool {
285 matches!(self, Self::WorkerConnectionLost { .. })
286 }
287
288 #[must_use]
290 pub const fn lock_poisoned(resource: &'static str) -> Self {
291 Self::LockPoisoned { resource }
292 }
293}
294
295#[derive(Clone)]
297pub struct ErrorTraceFields<'a> {
298 pub error_type: Cow<'a, str>,
300 pub store_error_type: Option<&'static str>,
302 pub reason: &'a dyn std::fmt::Display,
304}
305
306impl ServerError {
307 #[must_use]
309 pub fn trace_fields(&self) -> ErrorTraceFields<'_> {
310 match self {
311 Self::Config { message } => ErrorTraceFields {
312 error_type: Cow::Borrowed("Config"),
313 store_error_type: None,
314 reason: message,
315 },
316 Self::UnsafeDataRootAncestor { reason, .. } => ErrorTraceFields {
317 error_type: Cow::Borrowed("UnsafeDataRootAncestor"),
318 store_error_type: None,
319 reason,
320 },
321 Self::TransportBind { message, .. } => ErrorTraceFields {
322 error_type: Cow::Borrowed("TransportBind"),
323 store_error_type: None,
324 reason: message,
325 },
326 Self::Transport { message, .. } => ErrorTraceFields {
327 error_type: Cow::Borrowed("Transport"),
328 store_error_type: None,
329 reason: message,
330 },
331 Self::SignalListener { message, .. } => ErrorTraceFields {
332 error_type: Cow::Borrowed("SignalListener"),
333 store_error_type: None,
334 reason: message,
335 },
336 Self::Namespace { message } => ErrorTraceFields {
337 error_type: Cow::Borrowed("Namespace"),
338 store_error_type: None,
339 reason: message,
340 },
341 Self::EngineCall { source } => engine_trace_fields(source),
342 Self::StoreBackend { source } => store_trace_fields(source),
343 Self::Stream { failure } => ErrorTraceFields {
344 error_type: Cow::Borrowed("Stream"),
345 store_error_type: None,
346 reason: failure,
347 },
348 Self::WorkerDispatch { reason, .. } => ErrorTraceFields {
349 error_type: Cow::Borrowed("WorkerDispatch"),
350 store_error_type: None,
351 reason,
352 },
353 Self::WorkerConnectionLost { detail, .. } => ErrorTraceFields {
354 error_type: Cow::Borrowed("WorkerConnectionLost"),
355 store_error_type: None,
356 reason: detail,
357 },
358 Self::LockPoisoned { resource } => ErrorTraceFields {
359 error_type: Cow::Borrowed("LockPoisoned"),
360 store_error_type: None,
361 reason: resource,
362 },
363 Self::Wire { wire } => ErrorTraceFields {
364 error_type: wire
365 .error_type
366 .as_deref()
367 .map_or_else(|| Cow::Borrowed(wire.code.as_str()), Cow::Borrowed),
368 store_error_type: None,
369 reason: wire,
370 },
371 }
372 }
373}
374
375fn engine_trace_fields(source: &EngineError) -> ErrorTraceFields<'_> {
376 match source {
377 EngineError::WorkflowNotFound { .. } => simple_engine_fields("WorkflowNotFound", source),
378 EngineError::InvalidState { .. } => simple_engine_fields("InvalidState", source),
379 EngineError::ScheduleNotFound { .. } => simple_engine_fields("ScheduleNotFound", source),
380 EngineError::ShuttingDown => simple_engine_fields("ShuttingDown", source),
381 EngineError::Store(store) => store_trace_fields(store),
382 EngineError::Durability(durability) => match durability {
383 aion::durability::DurabilityError::Store(store) => store_trace_fields(store),
384 aion::durability::DurabilityError::NonDeterminism(_)
385 | aion::durability::DurabilityError::HistoryShape { .. }
386 | aion::durability::DurabilityError::SearchAttribute(_) => {
387 simple_engine_fields("Durability", source)
388 }
389 },
390 EngineError::MissingStore => simple_engine_fields("MissingStore", source),
391 EngineError::MissingVisibilityStore => {
392 simple_engine_fields("MissingVisibilityStore", source)
393 }
394 EngineError::ConflictingEventPublisher => {
395 simple_engine_fields("ConflictingEventPublisher", source)
396 }
397 EngineError::EventStreaming(_) => simple_engine_fields("EventStreaming", source),
398 EngineError::Load { .. } => simple_engine_fields("Load", source),
399 EngineError::UnknownVersion { .. } => simple_engine_fields("UnknownVersion", source),
400 EngineError::VersionPinned { .. } => simple_engine_fields("VersionPinned", source),
401 EngineError::RouteActive { .. } => simple_engine_fields("RouteActive", source),
402 EngineError::ManifestMismatch { .. } => simple_engine_fields("ManifestMismatch", source),
403 EngineError::Package(_) => simple_engine_fields("Package", source),
404 EngineError::Schedule { .. } => simple_engine_fields("Schedule", source),
405 EngineError::Runtime { .. } => simple_engine_fields("Runtime", source),
406 EngineError::RegistryPoisoned => simple_engine_fields("RegistryPoisoned", source),
407 EngineError::CatalogPoisoned => simple_engine_fields("CatalogPoisoned", source),
408 EngineError::NifRegistration { .. } => simple_engine_fields("NifRegistration", source),
409 EngineError::SignalRouter(_) => simple_engine_fields("SignalRouter", source),
410 EngineError::Query(query) => simple_engine_fields(query_error_type(query), source),
411 }
412}
413
414fn query_error_type(source: &aion::QueryError) -> &'static str {
416 match source {
417 aion::QueryError::UnknownQuery(_) => "UnknownQuery",
418 aion::QueryError::Timeout => "QueryTimeout",
419 aion::QueryError::NotRunning(_) => "QueryNotRunning",
420 aion::QueryError::Unknown(_) => "QueryUnknownWorkflow",
421 aion::QueryError::ReplyDropped => "QueryReplyDropped",
422 aion::QueryError::HandlerFailed { .. } => "QueryFailed",
423 aion::QueryError::Engine(_) => "QueryEngine",
424 }
425}
426
427fn simple_engine_fields<'a>(
428 error_type: &'static str,
429 source: &'a EngineError,
430) -> ErrorTraceFields<'a> {
431 ErrorTraceFields {
432 error_type: Cow::Borrowed(error_type),
433 store_error_type: None,
434 reason: source,
435 }
436}
437
438fn store_trace_fields(source: &StoreError) -> ErrorTraceFields<'_> {
439 ErrorTraceFields {
440 error_type: Cow::Borrowed("StoreError"),
441 store_error_type: Some(store_error_type(source)),
442 reason: source,
443 }
444}
445
446fn store_error_type(source: &StoreError) -> &'static str {
447 match source {
448 StoreError::SequenceConflict { .. } => "SequenceConflict",
449 StoreError::NotFound { .. } => "NotFound",
450 StoreError::NotOwner { .. } => "NotOwner",
451 StoreError::Backend(_) => "Backend",
452 StoreError::Serialization(_) => "Serialization",
453 }
454}
455
456fn wire_from_engine(source: &EngineError) -> WireError {
457 match source {
458 EngineError::WorkflowNotFound { .. } => {
459 WireError::not_found_with_type("WorkflowNotFound", source.to_string())
460 }
461 EngineError::InvalidState { reason } => {
465 WireError::invalid_state_with_type("InvalidState", reason.clone())
466 }
467 EngineError::ScheduleNotFound { .. } => {
468 WireError::not_found_with_type("ScheduleNotFound", source.to_string())
469 }
470 EngineError::ShuttingDown => {
471 WireError::not_running_with_type("ShuttingDown", source.to_string())
472 }
473 EngineError::Store(store) => wire_from_store(store),
474 EngineError::Durability(durability) => match durability {
475 aion::durability::DurabilityError::Store(store) => wire_from_store(store),
476 aion::durability::DurabilityError::NonDeterminism(_)
477 | aion::durability::DurabilityError::HistoryShape { .. }
478 | aion::durability::DurabilityError::SearchAttribute(_) => {
479 WireError::backend_with_type("Durability", source.to_string())
480 }
481 },
482 EngineError::MissingStore => {
483 WireError::backend_with_type("MissingStore", source.to_string())
484 }
485 EngineError::MissingVisibilityStore => {
486 WireError::backend_with_type("MissingVisibilityStore", source.to_string())
487 }
488 EngineError::ConflictingEventPublisher => {
489 WireError::backend_with_type("ConflictingEventPublisher", source.to_string())
490 }
491 EngineError::EventStreaming(_) => {
492 WireError::backend_with_type("EventStreaming", source.to_string())
493 }
494 EngineError::Load { .. } => WireError::backend_with_type("Load", source.to_string()),
495 EngineError::UnknownVersion { .. } => {
500 WireError::not_found_with_type("UnknownVersion", source.to_string())
501 }
502 EngineError::VersionPinned { .. } => {
503 WireError::version_pinned(source.to_string()).with_error_type("VersionPinned")
504 }
505 EngineError::RouteActive { .. } => {
506 WireError::version_pinned(source.to_string()).with_error_type("RouteActive")
507 }
508 EngineError::ManifestMismatch { .. } => {
509 WireError::invalid_input(source.to_string()).with_error_type("ManifestMismatch")
510 }
511 EngineError::Package(_) => WireError::backend_with_type("Package", source.to_string()),
512 EngineError::Schedule { .. } => {
513 WireError::backend_with_type("Schedule", source.to_string())
514 }
515 EngineError::Runtime { .. } => WireError::backend_with_type("Runtime", source.to_string()),
516 EngineError::CatalogPoisoned => {
517 WireError::backend_with_type("CatalogPoisoned", source.to_string())
518 }
519 EngineError::RegistryPoisoned => {
520 WireError::backend_with_type("RegistryPoisoned", source.to_string())
521 }
522 EngineError::NifRegistration { .. } => {
523 WireError::backend_with_type("NifRegistration", source.to_string())
524 }
525 EngineError::SignalRouter(_) => {
526 WireError::backend_with_type("SignalRouter", source.to_string())
527 }
528 EngineError::Query(query) => wire_from_query(query, source),
529 }
530}
531
532fn wire_from_query(query: &aion::QueryError, source: &EngineError) -> WireError {
538 match query {
539 aion::QueryError::UnknownQuery(_) => WireError::unknown_query(source.to_string()),
540 aion::QueryError::Timeout => WireError::query_timeout(source.to_string()),
541 aion::QueryError::NotRunning(_) | aion::QueryError::ReplyDropped => {
542 WireError::not_running_with_type(query_error_type(query), source.to_string())
543 }
544 aion::QueryError::Unknown(_) => {
545 WireError::not_found_with_type(query_error_type(query), source.to_string())
546 }
547 aion::QueryError::HandlerFailed { .. } => {
548 WireError::query_failed(source.to_string()).with_error_type(query_error_type(query))
549 }
550 aion::QueryError::Engine(_) => {
551 WireError::backend_with_type(query_error_type(query), source.to_string())
552 }
553 }
554}
555
556fn wire_from_store(source: &StoreError) -> WireError {
557 match source {
558 StoreError::SequenceConflict { .. } => WireError::new_with_type(
559 aion_proto::WireErrorCode::SequenceConflict,
560 "SequenceConflict",
561 source.to_string(),
562 ),
563 StoreError::NotFound { .. } => {
564 WireError::not_found_with_type("NotFound", source.to_string())
565 }
566 StoreError::NotOwner { .. } => {
567 WireError::not_owner(source.to_string()).with_error_type("NotOwner")
568 }
569 StoreError::Backend(_) => WireError::backend_with_type("Backend", source.to_string()),
570 StoreError::Serialization(_) => {
571 WireError::backend_with_type("Serialization", source.to_string())
572 }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::{ServerError, StreamFailure};
579 use aion::{EngineError, QueryError, engine_seam::EngineSeamError};
580 use aion_core::WorkflowId;
581 use aion_proto::WireErrorCode;
582
583 fn assert_send_sync<T: Send + Sync>() {}
584
585 #[test]
586 fn server_error_is_send_sync() {
587 assert_send_sync::<ServerError>();
588 }
589
590 #[test]
591 fn lagged_stream_maps_to_wire_lagged() {
592 let error = ServerError::Stream {
593 failure: StreamFailure::Lagged,
594 };
595
596 assert_eq!(error.to_wire_error().code, WireErrorCode::Lagged);
597 }
598
599 #[test]
603 fn not_owner_store_error_maps_to_wire_not_owner() {
604 let error = ServerError::StoreBackend {
605 source: aion_store::StoreError::NotOwner { shard: 3 },
606 };
607 let wire = error.to_wire_error();
608 assert_eq!(wire.code, WireErrorCode::NotOwner);
609 assert_eq!(wire.error_type.as_deref(), Some("NotOwner"));
610 }
611
612 fn workflow_id() -> WorkflowId {
613 WorkflowId::new(uuid::Uuid::from_u128(7))
614 }
615
616 fn query_wire(query: QueryError) -> aion_proto::WireError {
617 ServerError::EngineCall {
618 source: EngineError::Query(query),
619 }
620 .to_wire_error()
621 }
622
623 #[test]
627 fn every_query_error_arm_maps_to_its_pinned_wire_code() {
628 let arms: Vec<(QueryError, WireErrorCode, Option<&str>)> = vec![
629 (
630 QueryError::UnknownQuery(String::from("state")),
631 WireErrorCode::UnknownQuery,
632 None,
633 ),
634 (QueryError::Timeout, WireErrorCode::QueryTimeout, None),
635 (
636 QueryError::NotRunning(workflow_id()),
637 WireErrorCode::NotRunning,
638 Some("QueryNotRunning"),
639 ),
640 (
641 QueryError::Unknown(workflow_id()),
642 WireErrorCode::NotFound,
643 Some("QueryUnknownWorkflow"),
644 ),
645 (
647 QueryError::ReplyDropped,
648 WireErrorCode::NotRunning,
649 Some("QueryReplyDropped"),
650 ),
651 (
653 QueryError::HandlerFailed {
654 message: String::from("handler raised"),
655 },
656 WireErrorCode::QueryFailed,
657 Some("QueryFailed"),
658 ),
659 (
660 QueryError::Engine(EngineSeamError::Delivery {
661 reason: String::from("mailbox closed"),
662 }),
663 WireErrorCode::Backend,
664 Some("QueryEngine"),
665 ),
666 ];
667
668 let variant_count = arms
673 .iter()
674 .map(|(query, _, _)| match query {
675 QueryError::UnknownQuery(_) => 0,
676 QueryError::Timeout => 1,
677 QueryError::NotRunning(_) => 2,
678 QueryError::Unknown(_) => 3,
679 QueryError::ReplyDropped => 4,
680 QueryError::HandlerFailed { .. } => 5,
681 QueryError::Engine(_) => 6,
682 })
683 .collect::<std::collections::BTreeSet<usize>>()
684 .len();
685 assert_eq!(
686 arms.len(),
687 variant_count,
688 "every QueryError variant must appear exactly once in the pin list",
689 );
690 assert_eq!(variant_count, 7, "pin list must cover all 7 variants");
691
692 for (query, expected_code, expected_type) in arms {
693 let wire = query_wire(query.clone());
694 assert_eq!(
695 wire.code, expected_code,
696 "{query:?} must map to {expected_code:?}",
697 );
698 assert_eq!(
699 wire.error_type.as_deref(),
700 expected_type,
701 "{query:?} must carry error_type {expected_type:?}",
702 );
703 }
704 }
705
706 #[test]
709 fn handler_failed_trace_fields_use_query_failed_type() {
710 let error = ServerError::EngineCall {
711 source: EngineError::Query(QueryError::HandlerFailed {
712 message: String::from("handler raised"),
713 }),
714 };
715
716 assert_eq!(error.trace_fields().error_type, "QueryFailed");
717 }
718}