1use std::borrow::Cow;
4use std::net::SocketAddr;
5
6use aion::EngineError;
7use aion_proto::WireError;
8use aion_store::StoreError;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
13pub enum ServerError {
14 #[error("configuration error: {message}")]
16 Config {
17 message: String,
19 },
20
21 #[error("{transport} transport failed at {address}: {message}")]
23 TransportBind {
24 transport: &'static str,
26 address: SocketAddr,
28 message: String,
30 },
31
32 #[error("{transport} transport task failed: {message}")]
34 Transport {
35 transport: &'static str,
37 message: String,
39 },
40
41 #[error("{listener} listener failed: {message}")]
43 SignalListener {
44 listener: &'static str,
46 message: String,
48 },
49
50 #[error("namespace error: {message}")]
52 Namespace {
53 message: String,
55 },
56
57 #[error("engine call failed: {source}")]
59 EngineCall {
60 #[from]
62 source: EngineError,
63 },
64
65 #[error("store backend failed: {source}")]
67 StoreBackend {
68 #[from]
70 source: StoreError,
71 },
72
73 #[error("stream failure: {failure}")]
75 Stream {
76 failure: StreamFailure,
78 },
79
80 #[error(
82 "worker dispatch failed for namespace {namespace}, activity type {activity_type}: {reason}"
83 )]
84 WorkerDispatch {
85 namespace: String,
87 activity_type: String,
89 reason: String,
91 },
92
93 #[error("worker connection lost during dispatch on {channel}: {detail}")]
106 WorkerConnectionLost {
107 channel: String,
109 detail: String,
111 },
112
113 #[error("{resource} lock was poisoned")]
115 LockPoisoned {
116 resource: &'static str,
118 },
119
120 #[error("wire error: {wire}")]
122 Wire {
123 wire: WireError,
125 },
126}
127
128#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
130pub enum StreamFailure {
131 #[error("consumer lagged behind bounded buffer")]
133 Lagged,
134 #[error("subscriber connection closed")]
136 Closed,
137 #[error("engine event stream closed")]
139 UpstreamClosed,
140}
141
142impl From<WireError> for ServerError {
143 fn from(wire: WireError) -> Self {
144 Self::Wire { wire }
145 }
146}
147
148impl ServerError {
149 #[must_use]
152 pub fn to_wire_error(&self) -> WireError {
153 match self {
154 Self::Config { .. }
155 | Self::TransportBind { .. }
156 | Self::Transport { .. }
157 | Self::SignalListener { .. }
158 | Self::LockPoisoned { .. } => WireError::backend("server backend failure"),
159 Self::WorkerDispatch { .. } => WireError::backend("worker dispatch failed"),
160 Self::WorkerConnectionLost { .. } => {
161 WireError::backend("worker connection lost during dispatch")
162 }
163 Self::Namespace { message } => WireError::namespace_denied(message.clone()),
164 Self::EngineCall { source } => wire_from_engine(source),
165 Self::StoreBackend { source } => wire_from_store(source),
166 Self::Stream { failure } => match failure {
167 StreamFailure::Lagged => WireError::lagged("subscriber lagged behind"),
168 StreamFailure::Closed | StreamFailure::UpstreamClosed => {
169 WireError::backend("event stream closed")
170 }
171 },
172 Self::Wire { wire } => wire.clone(),
173 }
174 }
175
176 #[must_use]
178 pub const fn is_config(&self) -> bool {
179 matches!(self, Self::Config { .. })
180 }
181
182 #[must_use]
184 pub fn namespace_denied(message: impl Into<String>) -> Self {
185 Self::Namespace {
186 message: message.into(),
187 }
188 }
189
190 #[must_use]
198 pub fn placement_admission_denied(
199 namespace: &str,
200 worker_node: Option<&str>,
201 required: &std::collections::BTreeSet<String>,
202 ) -> Self {
203 let node = worker_node.unwrap_or("none");
204 let required = required
205 .iter()
206 .map(String::as_str)
207 .collect::<Vec<_>>()
208 .join(", ");
209 Self::namespace_denied(format!(
210 "worker registration rejected: namespace {namespace} is Pinned to node label(s) \
211 [{required}] but the worker advertises node {node}, which is not in the required set"
212 ))
213 }
214
215 #[must_use]
218 pub fn deploy_denied(message: impl Into<String>) -> Self {
219 Self::Wire {
220 wire: WireError::deploy_denied(message),
221 }
222 }
223
224 #[must_use]
226 pub const fn lagged_stream() -> Self {
227 Self::Stream {
228 failure: StreamFailure::Lagged,
229 }
230 }
231
232 #[must_use]
234 pub fn worker_dispatch(
235 namespace: impl Into<String>,
236 activity_type: impl Into<String>,
237 reason: impl Into<String>,
238 ) -> Self {
239 Self::WorkerDispatch {
240 namespace: namespace.into(),
241 activity_type: activity_type.into(),
242 reason: reason.into(),
243 }
244 }
245
246 #[must_use]
249 pub fn worker_connection_lost(channel: impl Into<String>, detail: impl Into<String>) -> Self {
250 Self::WorkerConnectionLost {
251 channel: channel.into(),
252 detail: detail.into(),
253 }
254 }
255
256 #[must_use]
262 pub const fn is_worker_connection_lost(&self) -> bool {
263 matches!(self, Self::WorkerConnectionLost { .. })
264 }
265
266 #[must_use]
268 pub const fn lock_poisoned(resource: &'static str) -> Self {
269 Self::LockPoisoned { resource }
270 }
271}
272
273#[derive(Clone)]
275pub struct ErrorTraceFields<'a> {
276 pub error_type: Cow<'a, str>,
278 pub store_error_type: Option<&'static str>,
280 pub reason: &'a dyn std::fmt::Display,
282}
283
284impl ServerError {
285 #[must_use]
287 pub fn trace_fields(&self) -> ErrorTraceFields<'_> {
288 match self {
289 Self::Config { message } => ErrorTraceFields {
290 error_type: Cow::Borrowed("Config"),
291 store_error_type: None,
292 reason: message,
293 },
294 Self::TransportBind { message, .. } => ErrorTraceFields {
295 error_type: Cow::Borrowed("TransportBind"),
296 store_error_type: None,
297 reason: message,
298 },
299 Self::Transport { message, .. } => ErrorTraceFields {
300 error_type: Cow::Borrowed("Transport"),
301 store_error_type: None,
302 reason: message,
303 },
304 Self::SignalListener { message, .. } => ErrorTraceFields {
305 error_type: Cow::Borrowed("SignalListener"),
306 store_error_type: None,
307 reason: message,
308 },
309 Self::Namespace { message } => ErrorTraceFields {
310 error_type: Cow::Borrowed("Namespace"),
311 store_error_type: None,
312 reason: message,
313 },
314 Self::EngineCall { source } => engine_trace_fields(source),
315 Self::StoreBackend { source } => store_trace_fields(source),
316 Self::Stream { failure } => ErrorTraceFields {
317 error_type: Cow::Borrowed("Stream"),
318 store_error_type: None,
319 reason: failure,
320 },
321 Self::WorkerDispatch { reason, .. } => ErrorTraceFields {
322 error_type: Cow::Borrowed("WorkerDispatch"),
323 store_error_type: None,
324 reason,
325 },
326 Self::WorkerConnectionLost { detail, .. } => ErrorTraceFields {
327 error_type: Cow::Borrowed("WorkerConnectionLost"),
328 store_error_type: None,
329 reason: detail,
330 },
331 Self::LockPoisoned { resource } => ErrorTraceFields {
332 error_type: Cow::Borrowed("LockPoisoned"),
333 store_error_type: None,
334 reason: resource,
335 },
336 Self::Wire { wire } => ErrorTraceFields {
337 error_type: wire
338 .error_type
339 .as_deref()
340 .map_or_else(|| Cow::Borrowed(wire.code.as_str()), Cow::Borrowed),
341 store_error_type: None,
342 reason: wire,
343 },
344 }
345 }
346}
347
348fn engine_trace_fields(source: &EngineError) -> ErrorTraceFields<'_> {
349 match source {
350 EngineError::WorkflowNotFound { .. } => simple_engine_fields("WorkflowNotFound", source),
351 EngineError::InvalidState { .. } => simple_engine_fields("InvalidState", source),
352 EngineError::ScheduleNotFound { .. } => simple_engine_fields("ScheduleNotFound", source),
353 EngineError::ShuttingDown => simple_engine_fields("ShuttingDown", source),
354 EngineError::Store(store) => store_trace_fields(store),
355 EngineError::Durability(durability) => match durability {
356 aion::durability::DurabilityError::Store(store) => store_trace_fields(store),
357 aion::durability::DurabilityError::NonDeterminism(_)
358 | aion::durability::DurabilityError::HistoryShape { .. }
359 | aion::durability::DurabilityError::SearchAttribute(_) => {
360 simple_engine_fields("Durability", source)
361 }
362 },
363 EngineError::MissingStore => simple_engine_fields("MissingStore", source),
364 EngineError::MissingVisibilityStore => {
365 simple_engine_fields("MissingVisibilityStore", source)
366 }
367 EngineError::ConflictingEventPublisher => {
368 simple_engine_fields("ConflictingEventPublisher", source)
369 }
370 EngineError::EventStreaming(_) => simple_engine_fields("EventStreaming", source),
371 EngineError::Load { .. } => simple_engine_fields("Load", source),
372 EngineError::UnknownVersion { .. } => simple_engine_fields("UnknownVersion", source),
373 EngineError::VersionPinned { .. } => simple_engine_fields("VersionPinned", source),
374 EngineError::RouteActive { .. } => simple_engine_fields("RouteActive", source),
375 EngineError::ManifestMismatch { .. } => simple_engine_fields("ManifestMismatch", source),
376 EngineError::Package(_) => simple_engine_fields("Package", source),
377 EngineError::Schedule { .. } => simple_engine_fields("Schedule", source),
378 EngineError::Runtime { .. } => simple_engine_fields("Runtime", source),
379 EngineError::RegistryPoisoned => simple_engine_fields("RegistryPoisoned", source),
380 EngineError::CatalogPoisoned => simple_engine_fields("CatalogPoisoned", source),
381 EngineError::NifRegistration { .. } => simple_engine_fields("NifRegistration", source),
382 EngineError::SignalRouter(_) => simple_engine_fields("SignalRouter", source),
383 EngineError::Query(query) => simple_engine_fields(query_error_type(query), source),
384 }
385}
386
387fn query_error_type(source: &aion::QueryError) -> &'static str {
389 match source {
390 aion::QueryError::UnknownQuery(_) => "UnknownQuery",
391 aion::QueryError::Timeout => "QueryTimeout",
392 aion::QueryError::NotRunning(_) => "QueryNotRunning",
393 aion::QueryError::Unknown(_) => "QueryUnknownWorkflow",
394 aion::QueryError::ReplyDropped => "QueryReplyDropped",
395 aion::QueryError::HandlerFailed { .. } => "QueryFailed",
396 aion::QueryError::Engine(_) => "QueryEngine",
397 }
398}
399
400fn simple_engine_fields<'a>(
401 error_type: &'static str,
402 source: &'a EngineError,
403) -> ErrorTraceFields<'a> {
404 ErrorTraceFields {
405 error_type: Cow::Borrowed(error_type),
406 store_error_type: None,
407 reason: source,
408 }
409}
410
411fn store_trace_fields(source: &StoreError) -> ErrorTraceFields<'_> {
412 ErrorTraceFields {
413 error_type: Cow::Borrowed("StoreError"),
414 store_error_type: Some(store_error_type(source)),
415 reason: source,
416 }
417}
418
419fn store_error_type(source: &StoreError) -> &'static str {
420 match source {
421 StoreError::SequenceConflict { .. } => "SequenceConflict",
422 StoreError::NotFound { .. } => "NotFound",
423 StoreError::NotOwner { .. } => "NotOwner",
424 StoreError::Backend(_) => "Backend",
425 StoreError::Serialization(_) => "Serialization",
426 }
427}
428
429fn wire_from_engine(source: &EngineError) -> WireError {
430 match source {
431 EngineError::WorkflowNotFound { .. } => {
432 WireError::not_found_with_type("WorkflowNotFound", source.to_string())
433 }
434 EngineError::InvalidState { reason } => {
438 WireError::invalid_state_with_type("InvalidState", reason.clone())
439 }
440 EngineError::ScheduleNotFound { .. } => {
441 WireError::not_found_with_type("ScheduleNotFound", source.to_string())
442 }
443 EngineError::ShuttingDown => {
444 WireError::not_running_with_type("ShuttingDown", source.to_string())
445 }
446 EngineError::Store(store) => wire_from_store(store),
447 EngineError::Durability(durability) => match durability {
448 aion::durability::DurabilityError::Store(store) => wire_from_store(store),
449 aion::durability::DurabilityError::NonDeterminism(_)
450 | aion::durability::DurabilityError::HistoryShape { .. }
451 | aion::durability::DurabilityError::SearchAttribute(_) => {
452 WireError::backend_with_type("Durability", source.to_string())
453 }
454 },
455 EngineError::MissingStore => {
456 WireError::backend_with_type("MissingStore", source.to_string())
457 }
458 EngineError::MissingVisibilityStore => {
459 WireError::backend_with_type("MissingVisibilityStore", source.to_string())
460 }
461 EngineError::ConflictingEventPublisher => {
462 WireError::backend_with_type("ConflictingEventPublisher", source.to_string())
463 }
464 EngineError::EventStreaming(_) => {
465 WireError::backend_with_type("EventStreaming", source.to_string())
466 }
467 EngineError::Load { .. } => WireError::backend_with_type("Load", source.to_string()),
468 EngineError::UnknownVersion { .. } => {
473 WireError::not_found_with_type("UnknownVersion", source.to_string())
474 }
475 EngineError::VersionPinned { .. } => {
476 WireError::version_pinned(source.to_string()).with_error_type("VersionPinned")
477 }
478 EngineError::RouteActive { .. } => {
479 WireError::version_pinned(source.to_string()).with_error_type("RouteActive")
480 }
481 EngineError::ManifestMismatch { .. } => {
482 WireError::invalid_input(source.to_string()).with_error_type("ManifestMismatch")
483 }
484 EngineError::Package(_) => WireError::backend_with_type("Package", source.to_string()),
485 EngineError::Schedule { .. } => {
486 WireError::backend_with_type("Schedule", source.to_string())
487 }
488 EngineError::Runtime { .. } => WireError::backend_with_type("Runtime", source.to_string()),
489 EngineError::CatalogPoisoned => {
490 WireError::backend_with_type("CatalogPoisoned", source.to_string())
491 }
492 EngineError::RegistryPoisoned => {
493 WireError::backend_with_type("RegistryPoisoned", source.to_string())
494 }
495 EngineError::NifRegistration { .. } => {
496 WireError::backend_with_type("NifRegistration", source.to_string())
497 }
498 EngineError::SignalRouter(_) => {
499 WireError::backend_with_type("SignalRouter", source.to_string())
500 }
501 EngineError::Query(query) => wire_from_query(query, source),
502 }
503}
504
505fn wire_from_query(query: &aion::QueryError, source: &EngineError) -> WireError {
511 match query {
512 aion::QueryError::UnknownQuery(_) => WireError::unknown_query(source.to_string()),
513 aion::QueryError::Timeout => WireError::query_timeout(source.to_string()),
514 aion::QueryError::NotRunning(_) | aion::QueryError::ReplyDropped => {
515 WireError::not_running_with_type(query_error_type(query), source.to_string())
516 }
517 aion::QueryError::Unknown(_) => {
518 WireError::not_found_with_type(query_error_type(query), source.to_string())
519 }
520 aion::QueryError::HandlerFailed { .. } => {
521 WireError::query_failed(source.to_string()).with_error_type(query_error_type(query))
522 }
523 aion::QueryError::Engine(_) => {
524 WireError::backend_with_type(query_error_type(query), source.to_string())
525 }
526 }
527}
528
529fn wire_from_store(source: &StoreError) -> WireError {
530 match source {
531 StoreError::SequenceConflict { .. } => WireError::new_with_type(
532 aion_proto::WireErrorCode::SequenceConflict,
533 "SequenceConflict",
534 source.to_string(),
535 ),
536 StoreError::NotFound { .. } => {
537 WireError::not_found_with_type("NotFound", source.to_string())
538 }
539 StoreError::NotOwner { .. } => {
540 WireError::not_owner(source.to_string()).with_error_type("NotOwner")
541 }
542 StoreError::Backend(_) => WireError::backend_with_type("Backend", source.to_string()),
543 StoreError::Serialization(_) => {
544 WireError::backend_with_type("Serialization", source.to_string())
545 }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::{ServerError, StreamFailure};
552 use aion::{EngineError, QueryError, engine_seam::EngineSeamError};
553 use aion_core::WorkflowId;
554 use aion_proto::WireErrorCode;
555
556 fn assert_send_sync<T: Send + Sync>() {}
557
558 #[test]
559 fn server_error_is_send_sync() {
560 assert_send_sync::<ServerError>();
561 }
562
563 #[test]
564 fn lagged_stream_maps_to_wire_lagged() {
565 let error = ServerError::Stream {
566 failure: StreamFailure::Lagged,
567 };
568
569 assert_eq!(error.to_wire_error().code, WireErrorCode::Lagged);
570 }
571
572 #[test]
576 fn not_owner_store_error_maps_to_wire_not_owner() {
577 let error = ServerError::StoreBackend {
578 source: aion_store::StoreError::NotOwner { shard: 3 },
579 };
580 let wire = error.to_wire_error();
581 assert_eq!(wire.code, WireErrorCode::NotOwner);
582 assert_eq!(wire.error_type.as_deref(), Some("NotOwner"));
583 }
584
585 fn workflow_id() -> WorkflowId {
586 WorkflowId::new(uuid::Uuid::from_u128(7))
587 }
588
589 fn query_wire(query: QueryError) -> aion_proto::WireError {
590 ServerError::EngineCall {
591 source: EngineError::Query(query),
592 }
593 .to_wire_error()
594 }
595
596 #[test]
600 fn every_query_error_arm_maps_to_its_pinned_wire_code() {
601 let arms: Vec<(QueryError, WireErrorCode, Option<&str>)> = vec![
602 (
603 QueryError::UnknownQuery(String::from("state")),
604 WireErrorCode::UnknownQuery,
605 None,
606 ),
607 (QueryError::Timeout, WireErrorCode::QueryTimeout, None),
608 (
609 QueryError::NotRunning(workflow_id()),
610 WireErrorCode::NotRunning,
611 Some("QueryNotRunning"),
612 ),
613 (
614 QueryError::Unknown(workflow_id()),
615 WireErrorCode::NotFound,
616 Some("QueryUnknownWorkflow"),
617 ),
618 (
620 QueryError::ReplyDropped,
621 WireErrorCode::NotRunning,
622 Some("QueryReplyDropped"),
623 ),
624 (
626 QueryError::HandlerFailed {
627 message: String::from("handler raised"),
628 },
629 WireErrorCode::QueryFailed,
630 Some("QueryFailed"),
631 ),
632 (
633 QueryError::Engine(EngineSeamError::Delivery {
634 reason: String::from("mailbox closed"),
635 }),
636 WireErrorCode::Backend,
637 Some("QueryEngine"),
638 ),
639 ];
640
641 let variant_count = arms
646 .iter()
647 .map(|(query, _, _)| match query {
648 QueryError::UnknownQuery(_) => 0,
649 QueryError::Timeout => 1,
650 QueryError::NotRunning(_) => 2,
651 QueryError::Unknown(_) => 3,
652 QueryError::ReplyDropped => 4,
653 QueryError::HandlerFailed { .. } => 5,
654 QueryError::Engine(_) => 6,
655 })
656 .collect::<std::collections::BTreeSet<usize>>()
657 .len();
658 assert_eq!(
659 arms.len(),
660 variant_count,
661 "every QueryError variant must appear exactly once in the pin list",
662 );
663 assert_eq!(variant_count, 7, "pin list must cover all 7 variants");
664
665 for (query, expected_code, expected_type) in arms {
666 let wire = query_wire(query.clone());
667 assert_eq!(
668 wire.code, expected_code,
669 "{query:?} must map to {expected_code:?}",
670 );
671 assert_eq!(
672 wire.error_type.as_deref(),
673 expected_type,
674 "{query:?} must carry error_type {expected_type:?}",
675 );
676 }
677 }
678
679 #[test]
682 fn handler_failed_trace_fields_use_query_failed_type() {
683 let error = ServerError::EngineCall {
684 source: EngineError::Query(QueryError::HandlerFailed {
685 message: String::from("handler raised"),
686 }),
687 };
688
689 assert_eq!(error.trace_fields().error_type, "QueryFailed");
690 }
691}