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("{resource} lock was poisoned")]
95 LockPoisoned {
96 resource: &'static str,
98 },
99
100 #[error("wire error: {wire}")]
102 Wire {
103 wire: WireError,
105 },
106}
107
108#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
110pub enum StreamFailure {
111 #[error("consumer lagged behind bounded buffer")]
113 Lagged,
114 #[error("subscriber connection closed")]
116 Closed,
117 #[error("engine event stream closed")]
119 UpstreamClosed,
120}
121
122impl From<WireError> for ServerError {
123 fn from(wire: WireError) -> Self {
124 Self::Wire { wire }
125 }
126}
127
128impl ServerError {
129 #[must_use]
132 pub fn to_wire_error(&self) -> WireError {
133 match self {
134 Self::Config { .. }
135 | Self::TransportBind { .. }
136 | Self::Transport { .. }
137 | Self::SignalListener { .. }
138 | Self::LockPoisoned { .. } => WireError::backend("server backend failure"),
139 Self::WorkerDispatch { .. } => WireError::backend("worker dispatch failed"),
140 Self::Namespace { message } => WireError::namespace_denied(message.clone()),
141 Self::EngineCall { source } => wire_from_engine(source),
142 Self::StoreBackend { source } => wire_from_store(source),
143 Self::Stream { failure } => match failure {
144 StreamFailure::Lagged => WireError::lagged("subscriber lagged behind"),
145 StreamFailure::Closed | StreamFailure::UpstreamClosed => {
146 WireError::backend("event stream closed")
147 }
148 },
149 Self::Wire { wire } => wire.clone(),
150 }
151 }
152
153 #[must_use]
155 pub const fn is_config(&self) -> bool {
156 matches!(self, Self::Config { .. })
157 }
158
159 #[must_use]
161 pub fn namespace_denied(message: impl Into<String>) -> Self {
162 Self::Namespace {
163 message: message.into(),
164 }
165 }
166
167 #[must_use]
170 pub fn deploy_denied(message: impl Into<String>) -> Self {
171 Self::Wire {
172 wire: WireError::deploy_denied(message),
173 }
174 }
175
176 #[must_use]
178 pub const fn lagged_stream() -> Self {
179 Self::Stream {
180 failure: StreamFailure::Lagged,
181 }
182 }
183
184 #[must_use]
186 pub fn worker_dispatch(
187 namespace: impl Into<String>,
188 activity_type: impl Into<String>,
189 reason: impl Into<String>,
190 ) -> Self {
191 Self::WorkerDispatch {
192 namespace: namespace.into(),
193 activity_type: activity_type.into(),
194 reason: reason.into(),
195 }
196 }
197
198 #[must_use]
200 pub const fn lock_poisoned(resource: &'static str) -> Self {
201 Self::LockPoisoned { resource }
202 }
203}
204
205#[derive(Clone)]
207pub struct ErrorTraceFields<'a> {
208 pub error_type: Cow<'a, str>,
210 pub store_error_type: Option<&'static str>,
212 pub reason: &'a dyn std::fmt::Display,
214}
215
216impl ServerError {
217 #[must_use]
219 pub fn trace_fields(&self) -> ErrorTraceFields<'_> {
220 match self {
221 Self::Config { message } => ErrorTraceFields {
222 error_type: Cow::Borrowed("Config"),
223 store_error_type: None,
224 reason: message,
225 },
226 Self::TransportBind { message, .. } => ErrorTraceFields {
227 error_type: Cow::Borrowed("TransportBind"),
228 store_error_type: None,
229 reason: message,
230 },
231 Self::Transport { message, .. } => ErrorTraceFields {
232 error_type: Cow::Borrowed("Transport"),
233 store_error_type: None,
234 reason: message,
235 },
236 Self::SignalListener { message, .. } => ErrorTraceFields {
237 error_type: Cow::Borrowed("SignalListener"),
238 store_error_type: None,
239 reason: message,
240 },
241 Self::Namespace { message } => ErrorTraceFields {
242 error_type: Cow::Borrowed("Namespace"),
243 store_error_type: None,
244 reason: message,
245 },
246 Self::EngineCall { source } => engine_trace_fields(source),
247 Self::StoreBackend { source } => store_trace_fields(source),
248 Self::Stream { failure } => ErrorTraceFields {
249 error_type: Cow::Borrowed("Stream"),
250 store_error_type: None,
251 reason: failure,
252 },
253 Self::WorkerDispatch { reason, .. } => ErrorTraceFields {
254 error_type: Cow::Borrowed("WorkerDispatch"),
255 store_error_type: None,
256 reason,
257 },
258 Self::LockPoisoned { resource } => ErrorTraceFields {
259 error_type: Cow::Borrowed("LockPoisoned"),
260 store_error_type: None,
261 reason: resource,
262 },
263 Self::Wire { wire } => ErrorTraceFields {
264 error_type: wire
265 .error_type
266 .as_deref()
267 .map_or_else(|| Cow::Borrowed(wire.code.as_str()), Cow::Borrowed),
268 store_error_type: None,
269 reason: wire,
270 },
271 }
272 }
273}
274
275fn engine_trace_fields(source: &EngineError) -> ErrorTraceFields<'_> {
276 match source {
277 EngineError::WorkflowNotFound { .. } => simple_engine_fields("WorkflowNotFound", source),
278 EngineError::ScheduleNotFound { .. } => simple_engine_fields("ScheduleNotFound", source),
279 EngineError::ShuttingDown => simple_engine_fields("ShuttingDown", source),
280 EngineError::Store(store) => store_trace_fields(store),
281 EngineError::Durability(durability) => match durability {
282 aion::durability::DurabilityError::Store(store) => store_trace_fields(store),
283 aion::durability::DurabilityError::NonDeterminism(_)
284 | aion::durability::DurabilityError::HistoryShape { .. }
285 | aion::durability::DurabilityError::SearchAttribute(_) => {
286 simple_engine_fields("Durability", source)
287 }
288 },
289 EngineError::MissingStore => simple_engine_fields("MissingStore", source),
290 EngineError::MissingVisibilityStore => {
291 simple_engine_fields("MissingVisibilityStore", source)
292 }
293 EngineError::ConflictingEventPublisher => {
294 simple_engine_fields("ConflictingEventPublisher", source)
295 }
296 EngineError::EventStreaming(_) => simple_engine_fields("EventStreaming", source),
297 EngineError::Load { .. } => simple_engine_fields("Load", source),
298 EngineError::UnknownVersion { .. } => simple_engine_fields("UnknownVersion", source),
299 EngineError::VersionPinned { .. } => simple_engine_fields("VersionPinned", source),
300 EngineError::RouteActive { .. } => simple_engine_fields("RouteActive", source),
301 EngineError::ManifestMismatch { .. } => simple_engine_fields("ManifestMismatch", source),
302 EngineError::Package(_) => simple_engine_fields("Package", source),
303 EngineError::Schedule { .. } => simple_engine_fields("Schedule", source),
304 EngineError::Runtime { .. } => simple_engine_fields("Runtime", source),
305 EngineError::RegistryPoisoned => simple_engine_fields("RegistryPoisoned", source),
306 EngineError::CatalogPoisoned => simple_engine_fields("CatalogPoisoned", source),
307 EngineError::NifRegistration { .. } => simple_engine_fields("NifRegistration", source),
308 EngineError::SignalRouter(_) => simple_engine_fields("SignalRouter", source),
309 EngineError::Query(query) => simple_engine_fields(query_error_type(query), source),
310 }
311}
312
313fn query_error_type(source: &aion::QueryError) -> &'static str {
315 match source {
316 aion::QueryError::UnknownQuery(_) => "UnknownQuery",
317 aion::QueryError::Timeout => "QueryTimeout",
318 aion::QueryError::NotRunning(_) => "QueryNotRunning",
319 aion::QueryError::Unknown(_) => "QueryUnknownWorkflow",
320 aion::QueryError::ReplyDropped => "QueryReplyDropped",
321 aion::QueryError::HandlerFailed { .. } => "QueryFailed",
322 aion::QueryError::Engine(_) => "QueryEngine",
323 }
324}
325
326fn simple_engine_fields<'a>(
327 error_type: &'static str,
328 source: &'a EngineError,
329) -> ErrorTraceFields<'a> {
330 ErrorTraceFields {
331 error_type: Cow::Borrowed(error_type),
332 store_error_type: None,
333 reason: source,
334 }
335}
336
337fn store_trace_fields(source: &StoreError) -> ErrorTraceFields<'_> {
338 ErrorTraceFields {
339 error_type: Cow::Borrowed("StoreError"),
340 store_error_type: Some(store_error_type(source)),
341 reason: source,
342 }
343}
344
345fn store_error_type(source: &StoreError) -> &'static str {
346 match source {
347 StoreError::SequenceConflict { .. } => "SequenceConflict",
348 StoreError::NotFound { .. } => "NotFound",
349 StoreError::Backend(_) => "Backend",
350 StoreError::Serialization(_) => "Serialization",
351 }
352}
353
354fn wire_from_engine(source: &EngineError) -> WireError {
355 match source {
356 EngineError::WorkflowNotFound { .. } => {
357 WireError::not_found_with_type("WorkflowNotFound", source.to_string())
358 }
359 EngineError::ScheduleNotFound { .. } => {
360 WireError::not_found_with_type("ScheduleNotFound", source.to_string())
361 }
362 EngineError::ShuttingDown => {
363 WireError::not_running_with_type("ShuttingDown", source.to_string())
364 }
365 EngineError::Store(store) => wire_from_store(store),
366 EngineError::Durability(durability) => match durability {
367 aion::durability::DurabilityError::Store(store) => wire_from_store(store),
368 aion::durability::DurabilityError::NonDeterminism(_)
369 | aion::durability::DurabilityError::HistoryShape { .. }
370 | aion::durability::DurabilityError::SearchAttribute(_) => {
371 WireError::backend_with_type("Durability", source.to_string())
372 }
373 },
374 EngineError::MissingStore => {
375 WireError::backend_with_type("MissingStore", source.to_string())
376 }
377 EngineError::MissingVisibilityStore => {
378 WireError::backend_with_type("MissingVisibilityStore", source.to_string())
379 }
380 EngineError::ConflictingEventPublisher => {
381 WireError::backend_with_type("ConflictingEventPublisher", source.to_string())
382 }
383 EngineError::EventStreaming(_) => {
384 WireError::backend_with_type("EventStreaming", source.to_string())
385 }
386 EngineError::Load { .. } => WireError::backend_with_type("Load", source.to_string()),
387 EngineError::UnknownVersion { .. } => {
392 WireError::not_found_with_type("UnknownVersion", source.to_string())
393 }
394 EngineError::VersionPinned { .. } => {
395 WireError::version_pinned(source.to_string()).with_error_type("VersionPinned")
396 }
397 EngineError::RouteActive { .. } => {
398 WireError::version_pinned(source.to_string()).with_error_type("RouteActive")
399 }
400 EngineError::ManifestMismatch { .. } => {
401 WireError::invalid_input(source.to_string()).with_error_type("ManifestMismatch")
402 }
403 EngineError::Package(_) => WireError::backend_with_type("Package", source.to_string()),
404 EngineError::Schedule { .. } => {
405 WireError::backend_with_type("Schedule", source.to_string())
406 }
407 EngineError::Runtime { .. } => WireError::backend_with_type("Runtime", source.to_string()),
408 EngineError::CatalogPoisoned => {
409 WireError::backend_with_type("CatalogPoisoned", source.to_string())
410 }
411 EngineError::RegistryPoisoned => {
412 WireError::backend_with_type("RegistryPoisoned", source.to_string())
413 }
414 EngineError::NifRegistration { .. } => {
415 WireError::backend_with_type("NifRegistration", source.to_string())
416 }
417 EngineError::SignalRouter(_) => {
418 WireError::backend_with_type("SignalRouter", source.to_string())
419 }
420 EngineError::Query(query) => wire_from_query(query, source),
421 }
422}
423
424fn wire_from_query(query: &aion::QueryError, source: &EngineError) -> WireError {
430 match query {
431 aion::QueryError::UnknownQuery(_) => WireError::unknown_query(source.to_string()),
432 aion::QueryError::Timeout => WireError::query_timeout(source.to_string()),
433 aion::QueryError::NotRunning(_) | aion::QueryError::ReplyDropped => {
434 WireError::not_running_with_type(query_error_type(query), source.to_string())
435 }
436 aion::QueryError::Unknown(_) => {
437 WireError::not_found_with_type(query_error_type(query), source.to_string())
438 }
439 aion::QueryError::HandlerFailed { .. } => {
440 WireError::query_failed(source.to_string()).with_error_type(query_error_type(query))
441 }
442 aion::QueryError::Engine(_) => {
443 WireError::backend_with_type(query_error_type(query), source.to_string())
444 }
445 }
446}
447
448fn wire_from_store(source: &StoreError) -> WireError {
449 match source {
450 StoreError::SequenceConflict { .. } => WireError::new_with_type(
451 aion_proto::WireErrorCode::SequenceConflict,
452 "SequenceConflict",
453 source.to_string(),
454 ),
455 StoreError::NotFound { .. } => {
456 WireError::not_found_with_type("NotFound", source.to_string())
457 }
458 StoreError::Backend(_) => WireError::backend_with_type("Backend", source.to_string()),
459 StoreError::Serialization(_) => {
460 WireError::backend_with_type("Serialization", source.to_string())
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::{ServerError, StreamFailure};
468 use aion::{EngineError, QueryError, engine_seam::EngineSeamError};
469 use aion_core::WorkflowId;
470 use aion_proto::WireErrorCode;
471
472 fn assert_send_sync<T: Send + Sync>() {}
473
474 #[test]
475 fn server_error_is_send_sync() {
476 assert_send_sync::<ServerError>();
477 }
478
479 #[test]
480 fn lagged_stream_maps_to_wire_lagged() {
481 let error = ServerError::Stream {
482 failure: StreamFailure::Lagged,
483 };
484
485 assert_eq!(error.to_wire_error().code, WireErrorCode::Lagged);
486 }
487
488 fn workflow_id() -> WorkflowId {
489 WorkflowId::new(uuid::Uuid::from_u128(7))
490 }
491
492 fn query_wire(query: QueryError) -> aion_proto::WireError {
493 ServerError::EngineCall {
494 source: EngineError::Query(query),
495 }
496 .to_wire_error()
497 }
498
499 #[test]
503 fn every_query_error_arm_maps_to_its_pinned_wire_code() {
504 let arms: Vec<(QueryError, WireErrorCode, Option<&str>)> = vec![
505 (
506 QueryError::UnknownQuery(String::from("state")),
507 WireErrorCode::UnknownQuery,
508 None,
509 ),
510 (QueryError::Timeout, WireErrorCode::QueryTimeout, None),
511 (
512 QueryError::NotRunning(workflow_id()),
513 WireErrorCode::NotRunning,
514 Some("QueryNotRunning"),
515 ),
516 (
517 QueryError::Unknown(workflow_id()),
518 WireErrorCode::NotFound,
519 Some("QueryUnknownWorkflow"),
520 ),
521 (
523 QueryError::ReplyDropped,
524 WireErrorCode::NotRunning,
525 Some("QueryReplyDropped"),
526 ),
527 (
529 QueryError::HandlerFailed {
530 message: String::from("handler raised"),
531 },
532 WireErrorCode::QueryFailed,
533 Some("QueryFailed"),
534 ),
535 (
536 QueryError::Engine(EngineSeamError::Delivery {
537 reason: String::from("mailbox closed"),
538 }),
539 WireErrorCode::Backend,
540 Some("QueryEngine"),
541 ),
542 ];
543
544 let variant_count = arms
549 .iter()
550 .map(|(query, _, _)| match query {
551 QueryError::UnknownQuery(_) => 0,
552 QueryError::Timeout => 1,
553 QueryError::NotRunning(_) => 2,
554 QueryError::Unknown(_) => 3,
555 QueryError::ReplyDropped => 4,
556 QueryError::HandlerFailed { .. } => 5,
557 QueryError::Engine(_) => 6,
558 })
559 .collect::<std::collections::BTreeSet<usize>>()
560 .len();
561 assert_eq!(
562 arms.len(),
563 variant_count,
564 "every QueryError variant must appear exactly once in the pin list",
565 );
566 assert_eq!(variant_count, 7, "pin list must cover all 7 variants");
567
568 for (query, expected_code, expected_type) in arms {
569 let wire = query_wire(query.clone());
570 assert_eq!(
571 wire.code, expected_code,
572 "{query:?} must map to {expected_code:?}",
573 );
574 assert_eq!(
575 wire.error_type.as_deref(),
576 expected_type,
577 "{query:?} must carry error_type {expected_type:?}",
578 );
579 }
580 }
581
582 #[test]
585 fn handler_failed_trace_fields_use_query_failed_type() {
586 let error = ServerError::EngineCall {
587 source: EngineError::Query(QueryError::HandlerFailed {
588 message: String::from("handler raised"),
589 }),
590 };
591
592 assert_eq!(error.trace_fields().error_type, "QueryFailed");
593 }
594}