1use core::fmt;
34use std::sync::Arc;
35use std::sync::atomic::{AtomicU64, Ordering};
36
37use crate::observability::SpanId;
38use crate::sync::LockError;
39use crate::types::symbol::{ObjectId, SymbolId};
40use crate::types::{CancelReason, RegionId, TaskId};
41
42pub mod recovery;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum ErrorKind {
47 Cancelled,
50 CancelTimeout,
52
53 DeadlineExceeded,
56 PollQuotaExhausted,
58 CostQuotaExhausted,
60
61 ChannelClosed,
64 ChannelFull,
66 ChannelEmpty,
68
69 ObligationLeak,
72 ObligationAlreadyResolved,
74 RegionFinalized,
76
77 RegionClosed,
80 TaskNotOwned,
82 AdmissionDenied,
84
85 InvalidEncodingParams,
88 DataTooLarge,
90 EncodingFailed,
92 CorruptedSymbol,
94
95 InsufficientSymbols,
98 DecodingFailed,
100 ObjectMismatch,
102 DuplicateSymbol,
104 ThresholdTimeout,
106
107 RoutingFailed,
110 DispatchFailed,
112 StreamEnded,
114 SinkRejected,
116 ConnectionLost,
118 ConnectionRefused,
120 ProtocolError,
122 RateLimited,
124 InvalidInput,
126 OperationFailed,
128
129 RecoveryFailed,
132 LeaseExpired,
134 LeaseRenewalFailed,
136 CoordinationFailed,
138 QuorumNotReached,
140 NodeUnavailable,
142 PartitionDetected,
144
145 Internal,
148 InvalidStateTransition,
150
151 ConfigError,
154
155 User,
158}
159
160impl ErrorKind {
161 #[must_use]
163 #[inline]
164 pub const fn asup_code(&self) -> Option<&'static str> {
165 match self {
166 Self::ObligationLeak => Some("ASUP-E101"),
167 Self::ObligationAlreadyResolved => Some("ASUP-E102"),
168 Self::RegionClosed => Some("ASUP-E003"),
169 Self::AdmissionDenied => Some("ASUP-E006"),
170 Self::ChannelClosed => Some("ASUP-E201"),
171 Self::CancelTimeout => Some("ASUP-E301"),
172 Self::ConfigError => Some("ASUP-E901"),
173 _ => None,
174 }
175 }
176
177 #[must_use]
179 #[inline]
180 pub const fn category(&self) -> ErrorCategory {
181 match self {
182 Self::Cancelled | Self::CancelTimeout => ErrorCategory::Cancellation,
183 Self::DeadlineExceeded | Self::PollQuotaExhausted | Self::CostQuotaExhausted => {
184 ErrorCategory::Budget
185 }
186 Self::ChannelClosed | Self::ChannelFull | Self::ChannelEmpty => ErrorCategory::Channel,
187 Self::ObligationLeak | Self::ObligationAlreadyResolved | Self::RegionFinalized => {
188 ErrorCategory::Obligation
189 }
190 Self::RegionClosed | Self::TaskNotOwned | Self::AdmissionDenied => {
191 ErrorCategory::Region
192 }
193 Self::InvalidEncodingParams
194 | Self::DataTooLarge
195 | Self::EncodingFailed
196 | Self::CorruptedSymbol => ErrorCategory::Encoding,
197 Self::InsufficientSymbols
198 | Self::DecodingFailed
199 | Self::ObjectMismatch
200 | Self::DuplicateSymbol
201 | Self::ThresholdTimeout => ErrorCategory::Decoding,
202 Self::RoutingFailed
203 | Self::DispatchFailed
204 | Self::StreamEnded
205 | Self::SinkRejected
206 | Self::ConnectionLost
207 | Self::ConnectionRefused
208 | Self::ProtocolError
209 | Self::RateLimited
210 | Self::InvalidInput
211 | Self::OperationFailed => ErrorCategory::Transport,
212 Self::RecoveryFailed
213 | Self::LeaseExpired
214 | Self::LeaseRenewalFailed
215 | Self::CoordinationFailed
216 | Self::QuorumNotReached
217 | Self::NodeUnavailable
218 | Self::PartitionDetected => ErrorCategory::Distributed,
219 Self::Internal | Self::InvalidStateTransition => ErrorCategory::Internal,
220 Self::ConfigError | Self::User => ErrorCategory::User,
221 }
222 }
223
224 #[must_use]
228 #[inline]
229 pub const fn recoverability(&self) -> Recoverability {
230 match self {
231 Self::ChannelFull
233 | Self::ChannelEmpty
234 | Self::AdmissionDenied
235 | Self::ConnectionLost
236 | Self::NodeUnavailable
237 | Self::QuorumNotReached
238 | Self::ThresholdTimeout
239 | Self::LeaseRenewalFailed
240 | Self::RateLimited => Recoverability::Transient,
241
242 Self::Cancelled
244 | Self::CancelTimeout
245 | Self::ChannelClosed
246 | Self::ObligationLeak
247 | Self::ObligationAlreadyResolved
248 | Self::RegionFinalized
249 | Self::RegionClosed
250 | Self::InvalidEncodingParams
251 | Self::DataTooLarge
252 | Self::ObjectMismatch
253 | Self::Internal
254 | Self::InvalidStateTransition
255 | Self::ProtocolError
256 | Self::ConnectionRefused
257 | Self::ConfigError
258 | Self::InvalidInput => Recoverability::Permanent,
259
260 Self::DeadlineExceeded
262 | Self::PollQuotaExhausted
263 | Self::CostQuotaExhausted
264 | Self::TaskNotOwned
265 | Self::EncodingFailed
266 | Self::CorruptedSymbol
267 | Self::InsufficientSymbols
268 | Self::DecodingFailed
269 | Self::DuplicateSymbol
270 | Self::RoutingFailed
271 | Self::DispatchFailed
272 | Self::StreamEnded
273 | Self::SinkRejected
274 | Self::RecoveryFailed
275 | Self::LeaseExpired
276 | Self::CoordinationFailed
277 | Self::PartitionDetected
278 | Self::OperationFailed
279 | Self::User => Recoverability::Unknown,
280 }
281 }
282
283 #[must_use]
285 #[inline]
286 pub const fn is_retryable(&self) -> bool {
287 matches!(self.recoverability(), Recoverability::Transient)
288 }
289
290 #[must_use]
295 #[inline]
296 pub const fn recovery_action(&self) -> RecoveryAction {
297 match self {
298 Self::ChannelFull | Self::ChannelEmpty => RecoveryAction::RetryImmediately,
300
301 Self::AdmissionDenied
303 | Self::ThresholdTimeout
304 | Self::QuorumNotReached
305 | Self::LeaseRenewalFailed
306 | Self::RateLimited => RecoveryAction::RetryWithBackoff(BackoffHint::DEFAULT),
307 Self::NodeUnavailable => RecoveryAction::RetryWithBackoff(BackoffHint::AGGRESSIVE),
308
309 Self::ConnectionLost | Self::StreamEnded => RecoveryAction::RetryWithNewConnection,
311
312 Self::Cancelled
314 | Self::CancelTimeout
315 | Self::DeadlineExceeded
316 | Self::PollQuotaExhausted
317 | Self::CostQuotaExhausted
318 | Self::ChannelClosed
319 | Self::RegionClosed
320 | Self::InvalidEncodingParams
321 | Self::DataTooLarge
322 | Self::ObjectMismatch
323 | Self::ConnectionRefused
324 | Self::ProtocolError
325 | Self::LeaseExpired
326 | Self::PartitionDetected
327 | Self::ConfigError
328 | Self::InvalidInput
329 | Self::OperationFailed => RecoveryAction::Propagate,
330
331 Self::ObligationLeak
333 | Self::ObligationAlreadyResolved
334 | Self::RegionFinalized
335 | Self::Internal
336 | Self::InvalidStateTransition => RecoveryAction::Escalate,
337
338 Self::TaskNotOwned
340 | Self::EncodingFailed
341 | Self::CorruptedSymbol
342 | Self::InsufficientSymbols
343 | Self::DecodingFailed
344 | Self::DuplicateSymbol
345 | Self::RoutingFailed
346 | Self::DispatchFailed
347 | Self::SinkRejected
348 | Self::RecoveryFailed
349 | Self::CoordinationFailed
350 | Self::User => RecoveryAction::Custom,
351 }
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
360pub enum Recoverability {
361 Transient,
363 Permanent,
365 Unknown,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
372pub enum RecoveryAction {
373 RetryImmediately,
375 RetryWithBackoff(BackoffHint),
377 RetryWithNewConnection,
379 Propagate,
381 Escalate,
383 Custom,
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
389pub struct BackoffHint {
390 pub initial_delay_ms: u32,
392 pub max_delay_ms: u32,
394 pub max_attempts: u8,
396}
397
398impl BackoffHint {
399 pub const DEFAULT: Self = Self {
401 initial_delay_ms: 100,
402 max_delay_ms: 30_000,
403 max_attempts: 5,
404 };
405
406 pub const AGGRESSIVE: Self = Self {
408 initial_delay_ms: 1_000,
409 max_delay_ms: 60_000,
410 max_attempts: 10,
411 };
412
413 pub const QUICK: Self = Self {
415 initial_delay_ms: 10,
416 max_delay_ms: 1_000,
417 max_attempts: 3,
418 };
419}
420
421impl Default for BackoffHint {
422 #[inline]
423 fn default() -> Self {
424 Self::DEFAULT
425 }
426}
427
428impl Recoverability {
429 #[must_use]
431 #[inline]
432 pub const fn should_retry(&self) -> bool {
433 matches!(self, Self::Transient)
434 }
435
436 #[must_use]
438 #[inline]
439 pub const fn is_permanent(&self) -> bool {
440 matches!(self, Self::Permanent)
441 }
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
446pub enum ErrorCategory {
447 Cancellation,
449 Budget,
451 Channel,
453 Obligation,
455 Region,
457 Encoding,
459 Decoding,
461 Transport,
463 Distributed,
465 Internal,
467 User,
469}
470
471#[derive(Debug, Clone, Default, PartialEq, Eq)]
473pub struct ErrorContext {
474 pub task_id: Option<TaskId>,
476 pub region_id: Option<RegionId>,
478 pub object_id: Option<ObjectId>,
480 pub symbol_id: Option<SymbolId>,
482 pub correlation_id: Option<u64>,
484 pub causal_chain: Vec<u64>,
486 pub span_id: Option<crate::observability::SpanId>,
488 pub parent_span_id: Option<crate::observability::SpanId>,
490 pub async_stack: Vec<String>,
492}
493
494impl ErrorContext {
495 #[must_use]
497 pub fn new() -> Self {
498 static NEXT_CORRELATION_ID: AtomicU64 = AtomicU64::new(1);
499 Self {
500 correlation_id: Some(NEXT_CORRELATION_ID.fetch_add(1, Ordering::Relaxed)),
501 ..Self::default()
502 }
503 }
504
505 #[must_use]
507 pub fn from_diagnostic_context(ctx: &crate::observability::DiagnosticContext) -> Self {
508 let mut error_ctx = Self::new();
509 error_ctx.task_id = ctx.task_id();
510 error_ctx.region_id = ctx.region_id();
511 error_ctx.span_id = ctx.span_id();
512 error_ctx.parent_span_id = ctx.parent_span_id();
513 error_ctx
514 }
515
516 #[must_use]
518 pub fn derive_child(&self, operation: &str) -> Self {
519 static NEXT_CORRELATION_ID: AtomicU64 = AtomicU64::new(1);
520 let child_correlation_id = NEXT_CORRELATION_ID.fetch_add(1, Ordering::Relaxed);
521
522 let mut causal_chain = self.causal_chain.clone();
523 if let Some(parent_id) = self.correlation_id {
524 causal_chain.push(parent_id);
525 }
526
527 let mut async_stack = self.async_stack.clone();
528 async_stack.push(operation.to_string());
529
530 Self {
531 task_id: self.task_id,
532 region_id: self.region_id,
533 object_id: self.object_id,
534 symbol_id: self.symbol_id,
535 correlation_id: Some(child_correlation_id),
536 causal_chain,
537 span_id: Some(SpanId::new()), parent_span_id: self.span_id,
539 async_stack,
540 }
541 }
542
543 #[must_use]
545 pub fn with_operation(mut self, operation: &str) -> Self {
546 self.async_stack.push(operation.to_string());
547 self
548 }
549
550 #[must_use]
552 pub fn with_span_context(mut self, span_id: SpanId, parent_span_id: Option<SpanId>) -> Self {
553 self.span_id = Some(span_id);
554 self.parent_span_id = parent_span_id;
555 self
556 }
557
558 #[must_use]
560 pub fn root_correlation_id(&self) -> Option<u64> {
561 self.causal_chain.first().copied().or(self.correlation_id)
562 }
563
564 #[must_use]
566 pub fn full_causal_chain(&self) -> Vec<u64> {
567 let mut chain = self.causal_chain.clone();
568 if let Some(id) = self.correlation_id {
569 chain.push(id);
570 }
571 chain
572 }
573
574 #[must_use]
576 pub fn format_async_stack(&self) -> String {
577 if self.async_stack.is_empty() {
578 "<no stack trace>".to_string()
579 } else {
580 self.async_stack.join(" -> ")
581 }
582 }
583}
584
585#[derive(Debug, Clone)]
587pub struct Error {
588 kind: ErrorKind,
589 message: Option<String>,
590 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
591 context: ErrorContext,
592}
593
594impl Error {
595 #[must_use]
597 #[inline]
598 pub fn new(kind: ErrorKind) -> Self {
599 Self {
600 kind,
601 message: None,
602 source: None,
603 context: ErrorContext::new(),
604 }
605 }
606
607 #[must_use]
609 #[inline]
610 pub const fn kind(&self) -> ErrorKind {
611 self.kind
612 }
613
614 #[must_use]
616 #[inline]
617 pub const fn is_cancelled(&self) -> bool {
618 matches!(self.kind, ErrorKind::Cancelled)
619 }
620
621 #[must_use]
623 #[inline]
624 pub const fn is_timeout(&self) -> bool {
625 matches!(
626 self.kind,
627 ErrorKind::DeadlineExceeded | ErrorKind::CancelTimeout
628 )
629 }
630
631 #[must_use]
633 #[inline]
634 pub fn with_message(mut self, msg: impl Into<String>) -> Self {
635 self.message = Some(msg.into());
636 self
637 }
638
639 #[must_use]
641 #[inline]
642 pub fn with_context(mut self, ctx: ErrorContext) -> Self {
643 self.context = ctx;
644 self
645 }
646
647 #[must_use]
649 #[inline]
650 pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
651 self.source = Some(Arc::new(source));
652 self
653 }
654
655 #[must_use]
657 pub fn from_cx(kind: ErrorKind, cx: &crate::cx::Cx) -> Self {
658 let diag_ctx = cx.diagnostic_context();
659 let error_ctx = ErrorContext::from_diagnostic_context(&diag_ctx)
660 .with_operation(&format!("Error::{:?}", kind));
661
662 Self::new(kind).with_context(error_ctx)
663 }
664
665 #[must_use]
667 pub fn propagate_across_async(mut self, operation: &str) -> Self {
668 self.context = self.context.derive_child(operation);
669 self
670 }
671
672 #[must_use]
674 pub fn with_operation(mut self, operation: &str) -> Self {
675 self.context = self.context.with_operation(operation);
676 self
677 }
678
679 #[must_use]
681 #[inline]
682 pub fn correlation_id(&self) -> Option<u64> {
683 self.context.correlation_id
684 }
685
686 #[must_use]
688 #[inline]
689 pub fn root_correlation_id(&self) -> Option<u64> {
690 self.context.root_correlation_id()
691 }
692
693 #[must_use]
695 #[inline]
696 pub fn causal_chain(&self) -> Vec<u64> {
697 self.context.full_causal_chain()
698 }
699
700 #[must_use]
702 #[inline]
703 pub fn async_stack(&self) -> String {
704 self.context.format_async_stack()
705 }
706
707 #[must_use]
709 #[inline]
710 pub fn cancelled(reason: &CancelReason) -> Self {
711 Self::new(ErrorKind::Cancelled).with_message(reason.to_string())
712 }
713
714 #[must_use]
716 #[inline]
717 pub const fn category(&self) -> ErrorCategory {
718 self.kind.category()
719 }
720
721 #[must_use]
723 #[inline]
724 pub const fn recoverability(&self) -> Recoverability {
725 self.kind.recoverability()
726 }
727
728 #[must_use]
730 #[inline]
731 pub const fn is_retryable(&self) -> bool {
732 self.kind.is_retryable()
733 }
734
735 #[must_use]
737 #[inline]
738 pub const fn recovery_action(&self) -> RecoveryAction {
739 self.kind.recovery_action()
740 }
741
742 #[must_use]
744 #[inline]
745 pub fn message(&self) -> Option<&str> {
746 self.message.as_deref()
747 }
748
749 #[must_use]
751 #[inline]
752 pub fn context(&self) -> &ErrorContext {
753 &self.context
754 }
755
756 #[must_use]
758 #[inline]
759 pub const fn is_encoding_error(&self) -> bool {
760 matches!(self.kind.category(), ErrorCategory::Encoding)
761 }
762
763 #[must_use]
765 #[inline]
766 pub const fn is_decoding_error(&self) -> bool {
767 matches!(self.kind.category(), ErrorCategory::Decoding)
768 }
769
770 #[must_use]
772 #[inline]
773 pub const fn is_transport_error(&self) -> bool {
774 matches!(self.kind.category(), ErrorCategory::Transport)
775 }
776
777 #[must_use]
779 #[inline]
780 pub const fn is_distributed_error(&self) -> bool {
781 matches!(self.kind.category(), ErrorCategory::Distributed)
782 }
783
784 #[must_use]
786 #[inline]
787 pub const fn is_connection_error(&self) -> bool {
788 matches!(
789 self.kind,
790 ErrorKind::ConnectionLost | ErrorKind::ConnectionRefused
791 )
792 }
793
794 #[must_use]
796 pub fn invalid_encoding_params(detail: impl Into<String>) -> Self {
797 Self::new(ErrorKind::InvalidEncodingParams).with_message(detail)
798 }
799
800 #[must_use]
802 pub fn data_too_large(actual: u64, max: u64) -> Self {
803 Self::new(ErrorKind::DataTooLarge)
804 .with_message(format!("data size {actual} exceeds maximum {max}"))
805 }
806
807 #[must_use]
809 pub fn insufficient_symbols(received: u32, needed: u32) -> Self {
810 Self::new(ErrorKind::InsufficientSymbols).with_message(format!(
811 "received {received} symbols, need at least {needed}"
812 ))
813 }
814
815 #[must_use]
817 pub fn decoding_failed(reason: impl Into<String>) -> Self {
818 Self::new(ErrorKind::DecodingFailed).with_message(reason)
819 }
820
821 #[must_use]
823 pub fn routing_failed(destination: impl Into<String>) -> Self {
824 Self::new(ErrorKind::RoutingFailed)
825 .with_message(format!("no route to destination: {}", destination.into()))
826 }
827
828 #[must_use]
830 pub fn lease_expired(lease_id: impl Into<String>) -> Self {
831 Self::new(ErrorKind::LeaseExpired)
832 .with_message(format!("lease expired: {}", lease_id.into()))
833 }
834
835 #[must_use]
837 pub fn quorum_not_reached(achieved: u32, needed: u32) -> Self {
838 Self::new(ErrorKind::QuorumNotReached)
839 .with_message(format!("achieved {achieved} of {needed} required"))
840 }
841
842 #[must_use]
844 pub fn node_unavailable(node_id: impl Into<String>) -> Self {
845 Self::new(ErrorKind::NodeUnavailable)
846 .with_message(format!("node unavailable: {}", node_id.into()))
847 }
848
849 #[must_use]
851 pub fn internal(detail: impl Into<String>) -> Self {
852 Self::new(ErrorKind::Internal).with_message(detail)
853 }
854}
855
856impl fmt::Display for Error {
857 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
858 if let Some(code) = self.kind.asup_code() {
859 write!(f, "[{code}] ")?;
860 }
861 write!(f, "{:?}", self.kind)?;
862 if let Some(msg) = &self.message {
863 write!(f, ": {msg}")?;
864 }
865 Ok(())
866 }
867}
868
869impl std::error::Error for Error {
870 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
871 self.source.as_ref().map(|e| e.as_ref() as _)
872 }
873}
874
875#[derive(Debug, Clone, PartialEq, Eq)]
877pub struct Cancelled {
878 pub reason: CancelReason,
880}
881
882impl From<Cancelled> for Error {
883 fn from(c: Cancelled) -> Self {
884 Self::cancelled(&c.reason)
885 }
886}
887
888#[derive(Debug)]
890pub enum SendError<T> {
891 Disconnected(T),
893 Full(T),
895 Cancelled(T),
897}
898
899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub enum RecvError {
902 Disconnected,
904 Empty,
906 Cancelled,
908}
909
910#[derive(Debug, Clone, Copy, PartialEq, Eq)]
912pub enum AcquireError {
913 Closed,
915}
916
917impl From<RecvError> for Error {
918 fn from(e: RecvError) -> Self {
919 match e {
920 RecvError::Disconnected => Self::new(ErrorKind::ChannelClosed),
921 RecvError::Empty => Self::new(ErrorKind::ChannelEmpty),
922 RecvError::Cancelled => Self::new(ErrorKind::Cancelled),
923 }
924 }
925}
926
927impl<T> From<SendError<T>> for Error {
928 fn from(e: SendError<T>) -> Self {
929 match e {
930 SendError::Disconnected(_) => Self::new(ErrorKind::ChannelClosed),
931 SendError::Full(_) => Self::new(ErrorKind::ChannelFull),
932 SendError::Cancelled(_) => Self::new(ErrorKind::Cancelled),
933 }
934 }
935}
936
937impl From<LockError> for Error {
938 fn from(e: LockError) -> Self {
939 match e {
940 LockError::Poisoned => Self::new(ErrorKind::InvalidStateTransition),
941 LockError::Cancelled => Self::new(ErrorKind::Cancelled),
942 LockError::TimedOut(_) => Self::new(ErrorKind::ThresholdTimeout),
943 LockError::PolledAfterCompletion => Self::new(ErrorKind::InvalidStateTransition),
944 }
945 }
946}
947
948#[allow(clippy::result_large_err)]
950pub trait ResultExt<T> {
951 fn context(self, msg: impl Into<String>) -> Result<T>;
953 fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T>;
955}
956
957impl<T, E: Into<Error>> ResultExt<T> for core::result::Result<T, E> {
958 fn context(self, msg: impl Into<String>) -> Result<T> {
959 self.map_err(|e| e.into().with_message(msg))
960 }
961
962 fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T> {
963 self.map_err(|e| e.into().with_message(f()))
964 }
965}
966
967#[allow(clippy::result_large_err)]
969pub type Result<T> = core::result::Result<T, Error>;
970
971#[cfg(test)]
972mod tests {
973 #![allow(
974 clippy::pedantic,
975 clippy::nursery,
976 clippy::expect_fun_call,
977 clippy::map_unwrap_or,
978 clippy::cast_possible_wrap,
979 clippy::future_not_send
980 )]
981 use super::*;
982 use std::error::Error as _;
983
984 #[derive(Debug)]
985 struct Underlying;
986
987 impl fmt::Display for Underlying {
988 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
989 write!(f, "underlying")
990 }
991 }
992
993 impl std::error::Error for Underlying {}
994
995 #[test]
996 fn display_without_message() {
997 let err = Error::new(ErrorKind::Internal);
998 assert_eq!(err.to_string(), "Internal");
999 }
1000
1001 #[test]
1002 fn display_with_message() {
1003 let err = Error::new(ErrorKind::ChannelEmpty).with_message("no messages");
1004 assert_eq!(err.to_string(), "ChannelEmpty: no messages");
1005 }
1006
1007 #[test]
1008 fn asup_codes_are_stable_for_live_error_kinds() {
1009 let cases = [
1010 (ErrorKind::ObligationLeak, Some("ASUP-E101")),
1011 (ErrorKind::ObligationAlreadyResolved, Some("ASUP-E102")),
1012 (ErrorKind::RegionClosed, Some("ASUP-E003")),
1013 (ErrorKind::AdmissionDenied, Some("ASUP-E006")),
1014 (ErrorKind::ChannelClosed, Some("ASUP-E201")),
1015 (ErrorKind::CancelTimeout, Some("ASUP-E301")),
1016 (ErrorKind::ConfigError, Some("ASUP-E901")),
1017 (ErrorKind::ChannelEmpty, None),
1018 ];
1019
1020 for (kind, expected) in cases {
1021 assert_eq!(kind.asup_code(), expected, "{kind:?}");
1022 }
1023 }
1024
1025 #[test]
1026 fn display_prefixes_live_asup_codes() {
1027 let leak = Error::new(ErrorKind::ObligationLeak);
1028 assert_eq!(leak.to_string(), "[ASUP-E101] ObligationLeak");
1029
1030 let double_resolve = Error::new(ErrorKind::ObligationAlreadyResolved);
1031 assert_eq!(
1032 double_resolve.to_string(),
1033 "[ASUP-E102] ObligationAlreadyResolved"
1034 );
1035
1036 let channel = Error::new(ErrorKind::ChannelClosed);
1037 assert_eq!(channel.to_string(), "[ASUP-E201] ChannelClosed");
1038
1039 let region = Error::new(ErrorKind::RegionClosed);
1040 assert_eq!(region.to_string(), "[ASUP-E003] RegionClosed");
1041
1042 let admission = Error::new(ErrorKind::AdmissionDenied);
1043 assert_eq!(admission.to_string(), "[ASUP-E006] AdmissionDenied");
1044
1045 let drain = Error::new(ErrorKind::CancelTimeout);
1046 assert_eq!(drain.to_string(), "[ASUP-E301] CancelTimeout");
1047
1048 let config = Error::new(ErrorKind::ConfigError).with_message("min_threads exceeds max");
1049 assert_eq!(
1050 config.to_string(),
1051 "[ASUP-E901] ConfigError: min_threads exceeds max"
1052 );
1053 }
1054
1055 #[test]
1056 fn source_chain_is_exposed() {
1057 let err = Error::new(ErrorKind::User)
1058 .with_message("outer")
1059 .with_source(Underlying);
1060 let source = err.source().expect("source missing");
1061 assert_eq!(source.to_string(), "underlying");
1062 }
1063
1064 #[test]
1065 fn from_recv_error() {
1066 let disconnected: Error = RecvError::Disconnected.into();
1067 assert_eq!(disconnected.kind(), ErrorKind::ChannelClosed);
1068
1069 let empty: Error = RecvError::Empty.into();
1070 assert_eq!(empty.kind(), ErrorKind::ChannelEmpty);
1071 }
1072
1073 #[test]
1074 fn from_send_error() {
1075 let disconnected: Error = SendError::Disconnected(()).into();
1076 assert_eq!(disconnected.kind(), ErrorKind::ChannelClosed);
1077
1078 let full: Error = SendError::Full(()).into();
1079 assert_eq!(full.kind(), ErrorKind::ChannelFull);
1080 }
1081
1082 #[test]
1083 fn result_ext_adds_message() {
1084 let res: core::result::Result<(), RecvError> = Err(RecvError::Empty);
1085 let err = res.context("recv failed").expect_err("expected err");
1086 assert_eq!(err.kind(), ErrorKind::ChannelEmpty);
1087 assert_eq!(err.to_string(), "ChannelEmpty: recv failed");
1088 }
1089
1090 #[test]
1091 fn predicates_match_kind() {
1092 let cancel = Error::new(ErrorKind::Cancelled);
1093 assert!(cancel.is_cancelled());
1094 assert!(!cancel.is_timeout());
1095
1096 let timeout = Error::new(ErrorKind::DeadlineExceeded);
1097 assert!(!timeout.is_cancelled());
1098 assert!(timeout.is_timeout());
1099 }
1100
1101 #[test]
1102 fn recovery_action_backoff() {
1103 let action = ErrorKind::ThresholdTimeout.recovery_action();
1104 assert!(matches!(action, RecoveryAction::RetryWithBackoff(_)));
1105 }
1106
1107 #[test]
1108 fn error_context_default() {
1109 let err = Error::new(ErrorKind::Internal);
1110 assert!(err.context().task_id.is_none());
1111 }
1112
1113 #[test]
1114 fn error_with_full_context() {
1115 use crate::util::ArenaIndex;
1116
1117 let task_id = TaskId::from_arena(ArenaIndex::new(1, 0));
1118 let region_id = RegionId::from_arena(ArenaIndex::new(2, 0));
1119 let object_id = ObjectId::new_for_test(123);
1120 let symbol_id = SymbolId::new_for_test(123, 0, 1);
1121
1122 let ctx = ErrorContext {
1123 task_id: Some(task_id),
1124 region_id: Some(region_id),
1125 object_id: Some(object_id),
1126 symbol_id: Some(symbol_id),
1127 correlation_id: None,
1128 causal_chain: Vec::new(),
1129 span_id: None,
1130 parent_span_id: None,
1131 async_stack: Vec::new(),
1132 };
1133
1134 let err = Error::new(ErrorKind::Internal).with_context(ctx);
1135
1136 assert_eq!(err.context().task_id, Some(task_id));
1137 assert_eq!(err.context().region_id, Some(region_id));
1138 assert_eq!(err.context().object_id, Some(object_id));
1139 assert_eq!(err.context().symbol_id, Some(symbol_id));
1140 }
1141
1142 #[test]
1145 fn error_kind_category_coverage() {
1146 use ErrorCategory::*;
1147 let cases: &[(ErrorKind, ErrorCategory)] = &[
1148 (ErrorKind::Cancelled, Cancellation),
1149 (ErrorKind::CancelTimeout, Cancellation),
1150 (ErrorKind::DeadlineExceeded, Budget),
1151 (ErrorKind::PollQuotaExhausted, Budget),
1152 (ErrorKind::CostQuotaExhausted, Budget),
1153 (ErrorKind::ChannelClosed, Channel),
1154 (ErrorKind::ChannelFull, Channel),
1155 (ErrorKind::ChannelEmpty, Channel),
1156 (ErrorKind::ObligationLeak, Obligation),
1157 (ErrorKind::ObligationAlreadyResolved, Obligation),
1158 (ErrorKind::RegionClosed, Region),
1159 (ErrorKind::TaskNotOwned, Region),
1160 (ErrorKind::AdmissionDenied, Region),
1161 (ErrorKind::InvalidEncodingParams, Encoding),
1162 (ErrorKind::DataTooLarge, Encoding),
1163 (ErrorKind::EncodingFailed, Encoding),
1164 (ErrorKind::CorruptedSymbol, Encoding),
1165 (ErrorKind::InsufficientSymbols, Decoding),
1166 (ErrorKind::DecodingFailed, Decoding),
1167 (ErrorKind::ObjectMismatch, Decoding),
1168 (ErrorKind::DuplicateSymbol, Decoding),
1169 (ErrorKind::ThresholdTimeout, Decoding),
1170 (ErrorKind::RoutingFailed, Transport),
1171 (ErrorKind::DispatchFailed, Transport),
1172 (ErrorKind::StreamEnded, Transport),
1173 (ErrorKind::SinkRejected, Transport),
1174 (ErrorKind::ConnectionLost, Transport),
1175 (ErrorKind::ConnectionRefused, Transport),
1176 (ErrorKind::ProtocolError, Transport),
1177 (ErrorKind::RecoveryFailed, Distributed),
1178 (ErrorKind::LeaseExpired, Distributed),
1179 (ErrorKind::LeaseRenewalFailed, Distributed),
1180 (ErrorKind::CoordinationFailed, Distributed),
1181 (ErrorKind::QuorumNotReached, Distributed),
1182 (ErrorKind::NodeUnavailable, Distributed),
1183 (ErrorKind::PartitionDetected, Distributed),
1184 (ErrorKind::Internal, Internal),
1185 (ErrorKind::InvalidStateTransition, Internal),
1186 (ErrorKind::ConfigError, User),
1187 (ErrorKind::User, User),
1188 ];
1189 for (kind, expected) in cases {
1190 assert_eq!(kind.category(), *expected, "{kind:?}");
1191 }
1192 }
1193
1194 #[test]
1195 fn error_kind_recoverability_classification() {
1196 for kind in [
1198 ErrorKind::ChannelFull,
1199 ErrorKind::ChannelEmpty,
1200 ErrorKind::AdmissionDenied,
1201 ErrorKind::ConnectionLost,
1202 ErrorKind::NodeUnavailable,
1203 ErrorKind::QuorumNotReached,
1204 ErrorKind::ThresholdTimeout,
1205 ErrorKind::LeaseRenewalFailed,
1206 ] {
1207 assert_eq!(kind.recoverability(), Recoverability::Transient, "{kind:?}");
1208 assert!(kind.is_retryable(), "{kind:?} should be retryable");
1209 }
1210
1211 for kind in [
1213 ErrorKind::Cancelled,
1214 ErrorKind::ChannelClosed,
1215 ErrorKind::ObligationLeak,
1216 ErrorKind::Internal,
1217 ErrorKind::ConnectionRefused,
1218 ErrorKind::ConfigError,
1219 ] {
1220 assert_eq!(kind.recoverability(), Recoverability::Permanent, "{kind:?}");
1221 assert!(!kind.is_retryable(), "{kind:?} should not be retryable");
1222 }
1223
1224 for kind in [
1226 ErrorKind::DeadlineExceeded,
1227 ErrorKind::EncodingFailed,
1228 ErrorKind::CorruptedSymbol,
1229 ErrorKind::User,
1230 ] {
1231 assert_eq!(kind.recoverability(), Recoverability::Unknown, "{kind:?}");
1232 assert!(!kind.is_retryable(), "{kind:?} Unknown is not retryable");
1233 }
1234 }
1235
1236 #[test]
1237 fn recoverability_predicates() {
1238 assert!(Recoverability::Transient.should_retry());
1239 assert!(!Recoverability::Transient.is_permanent());
1240
1241 assert!(!Recoverability::Permanent.should_retry());
1242 assert!(Recoverability::Permanent.is_permanent());
1243
1244 assert!(!Recoverability::Unknown.should_retry());
1245 assert!(!Recoverability::Unknown.is_permanent());
1246 }
1247
1248 #[test]
1249 fn recovery_action_variants() {
1250 assert!(matches!(
1251 ErrorKind::ChannelFull.recovery_action(),
1252 RecoveryAction::RetryImmediately
1253 ));
1254 assert!(matches!(
1255 ErrorKind::AdmissionDenied.recovery_action(),
1256 RecoveryAction::RetryWithBackoff(_)
1257 ));
1258 assert!(matches!(
1259 ErrorKind::NodeUnavailable.recovery_action(),
1260 RecoveryAction::RetryWithBackoff(_)
1261 ));
1262 assert!(matches!(
1263 ErrorKind::ConnectionLost.recovery_action(),
1264 RecoveryAction::RetryWithNewConnection
1265 ));
1266 assert!(matches!(
1267 ErrorKind::Cancelled.recovery_action(),
1268 RecoveryAction::Propagate
1269 ));
1270 assert!(matches!(
1271 ErrorKind::ObligationLeak.recovery_action(),
1272 RecoveryAction::Escalate
1273 ));
1274 assert!(matches!(
1275 ErrorKind::User.recovery_action(),
1276 RecoveryAction::Custom
1277 ));
1278 }
1279
1280 #[test]
1281 fn backoff_hint_constants() {
1282 let d = BackoffHint::DEFAULT;
1283 assert_eq!(d.initial_delay_ms, 100);
1284 assert_eq!(d.max_delay_ms, 30_000);
1285 assert_eq!(d.max_attempts, 5);
1286
1287 let a = BackoffHint::AGGRESSIVE;
1288 assert!(a.initial_delay_ms > d.initial_delay_ms);
1289 assert!(a.max_attempts > d.max_attempts);
1290
1291 let q = BackoffHint::QUICK;
1292 assert!(q.initial_delay_ms < d.initial_delay_ms);
1293 assert!(q.max_attempts < d.max_attempts);
1294
1295 assert_eq!(BackoffHint::default(), BackoffHint::DEFAULT);
1296 }
1297
1298 #[test]
1301 fn error_data_too_large() {
1302 let err = Error::data_too_large(2000, 1000);
1303 assert_eq!(err.kind(), ErrorKind::DataTooLarge);
1304 let msg = err.to_string();
1305 assert!(msg.contains("2000"), "{msg}");
1306 assert!(msg.contains("1000"), "{msg}");
1307 }
1308
1309 #[test]
1310 fn error_insufficient_symbols() {
1311 let err = Error::insufficient_symbols(5, 10);
1312 assert_eq!(err.kind(), ErrorKind::InsufficientSymbols);
1313 let msg = err.to_string();
1314 assert!(msg.contains('5'), "{msg}");
1315 assert!(msg.contains("10"), "{msg}");
1316 }
1317
1318 #[test]
1319 fn error_routing_failed() {
1320 let err = Error::routing_failed("node-7");
1321 assert_eq!(err.kind(), ErrorKind::RoutingFailed);
1322 assert!(err.to_string().contains("node-7"));
1323 }
1324
1325 #[test]
1326 fn error_lease_expired() {
1327 let err = Error::lease_expired("lease-42");
1328 assert_eq!(err.kind(), ErrorKind::LeaseExpired);
1329 assert!(err.to_string().contains("lease-42"));
1330 }
1331
1332 #[test]
1333 fn error_quorum_not_reached() {
1334 let err = Error::quorum_not_reached(2, 3);
1335 assert_eq!(err.kind(), ErrorKind::QuorumNotReached);
1336 let msg = err.to_string();
1337 assert!(msg.contains('2'), "{msg}");
1338 assert!(msg.contains('3'), "{msg}");
1339 }
1340
1341 #[test]
1342 fn error_node_unavailable() {
1343 let err = Error::node_unavailable("node-1");
1344 assert_eq!(err.kind(), ErrorKind::NodeUnavailable);
1345 assert!(err.to_string().contains("node-1"));
1346 }
1347
1348 #[test]
1349 fn error_internal() {
1350 let err = Error::internal("bug found");
1351 assert_eq!(err.kind(), ErrorKind::Internal);
1352 assert!(err.to_string().contains("bug found"));
1353 }
1354
1355 #[test]
1358 fn error_is_predicates() {
1359 assert!(Error::new(ErrorKind::EncodingFailed).is_encoding_error());
1360 assert!(!Error::new(ErrorKind::DecodingFailed).is_encoding_error());
1361
1362 assert!(Error::new(ErrorKind::InsufficientSymbols).is_decoding_error());
1363 assert!(!Error::new(ErrorKind::EncodingFailed).is_decoding_error());
1364
1365 assert!(Error::new(ErrorKind::RoutingFailed).is_transport_error());
1366 assert!(!Error::new(ErrorKind::Internal).is_transport_error());
1367
1368 assert!(Error::new(ErrorKind::QuorumNotReached).is_distributed_error());
1369 assert!(!Error::new(ErrorKind::ChannelFull).is_distributed_error());
1370
1371 assert!(Error::new(ErrorKind::ConnectionLost).is_connection_error());
1372 assert!(Error::new(ErrorKind::ConnectionRefused).is_connection_error());
1373 assert!(!Error::new(ErrorKind::RoutingFailed).is_connection_error());
1374 }
1375
1376 #[test]
1377 fn error_cancel_timeout_is_timeout() {
1378 assert!(Error::new(ErrorKind::CancelTimeout).is_timeout());
1379 assert!(!Error::new(ErrorKind::CancelTimeout).is_cancelled());
1380 }
1381
1382 #[test]
1385 fn recv_error_cancelled_conversion() {
1386 let err: Error = RecvError::Cancelled.into();
1387 assert_eq!(err.kind(), ErrorKind::Cancelled);
1388 }
1389
1390 #[test]
1391 fn send_error_cancelled_conversion() {
1392 let err: Error = SendError::Cancelled(42u32).into();
1393 assert_eq!(err.kind(), ErrorKind::Cancelled);
1394 }
1395
1396 #[test]
1397 fn cancelled_struct_into_error() {
1398 let reason = CancelReason::user("test cancel");
1399 let cancelled = Cancelled { reason };
1400 let err: Error = cancelled.into();
1401 assert_eq!(err.kind(), ErrorKind::Cancelled);
1402 assert!(err.to_string().contains("Cancelled"));
1403 }
1404
1405 #[test]
1406 fn result_ext_with_context_lazy() {
1407 let res: core::result::Result<(), RecvError> = Err(RecvError::Empty);
1408 let err = res
1409 .with_context(|| format!("lazy {}", "context"))
1410 .expect_err("expected err");
1411 assert_eq!(err.kind(), ErrorKind::ChannelEmpty);
1412 assert!(err.to_string().contains("lazy context"));
1413 }
1414
1415 #[test]
1418 fn error_category_debug() {
1419 for cat in [
1420 ErrorCategory::Cancellation,
1421 ErrorCategory::Budget,
1422 ErrorCategory::Channel,
1423 ErrorCategory::Obligation,
1424 ErrorCategory::Region,
1425 ErrorCategory::Encoding,
1426 ErrorCategory::Decoding,
1427 ErrorCategory::Transport,
1428 ErrorCategory::Distributed,
1429 ErrorCategory::Internal,
1430 ErrorCategory::User,
1431 ] {
1432 let dbg = format!("{cat:?}");
1433 assert!(!dbg.is_empty());
1434 }
1435 }
1436
1437 #[test]
1438 fn acquire_error_debug_eq() {
1439 let err = AcquireError::Closed;
1440 let dbg = format!("{err:?}");
1441 assert!(dbg.contains("Closed"), "{dbg}");
1442 assert_eq!(err, AcquireError::Closed);
1443 }
1444
1445 #[test]
1446 fn error_clone() {
1447 let err = Error::new(ErrorKind::Internal).with_message("clone me");
1448 let cloned = err.clone();
1449 assert_eq!(cloned.kind(), ErrorKind::Internal);
1450 assert_eq!(cloned.to_string(), err.to_string());
1451 }
1452
1453 #[test]
1454 fn error_no_message() {
1455 let err = Error::new(ErrorKind::User);
1456 assert!(err.message().is_none());
1457 }
1458
1459 #[test]
1460 fn error_source_none_without_with_source() {
1461 let err = Error::new(ErrorKind::User);
1462 assert!(err.source().is_none());
1463 }
1464
1465 #[test]
1468 fn error_kind_copy_hash() {
1469 use std::collections::HashSet;
1470 let kind = ErrorKind::Internal;
1471 let copied = kind;
1472 assert_eq!(copied, ErrorKind::Internal);
1473
1474 let mut set = HashSet::new();
1475 set.insert(ErrorKind::Cancelled);
1476 set.insert(ErrorKind::DeadlineExceeded);
1477 set.insert(ErrorKind::Cancelled); assert_eq!(set.len(), 2);
1479 }
1480
1481 #[test]
1482 fn recoverability_copy_hash_eq() {
1483 use std::collections::HashSet;
1484 let r = Recoverability::Transient;
1485 let copied = r;
1486 assert_eq!(copied, Recoverability::Transient);
1487 assert_ne!(r, Recoverability::Permanent);
1488
1489 let mut set = HashSet::new();
1490 set.insert(Recoverability::Transient);
1491 set.insert(Recoverability::Permanent);
1492 set.insert(Recoverability::Unknown);
1493 assert_eq!(set.len(), 3);
1494 }
1495
1496 #[test]
1497 fn recovery_action_copy_hash() {
1498 use std::collections::HashSet;
1499 let action = RecoveryAction::Propagate;
1500 let copied = action;
1501 assert_eq!(copied, RecoveryAction::Propagate);
1502
1503 let mut set = HashSet::new();
1504 set.insert(RecoveryAction::RetryImmediately);
1505 set.insert(RecoveryAction::Propagate);
1506 set.insert(RecoveryAction::Escalate);
1507 set.insert(RecoveryAction::Custom);
1508 assert_eq!(set.len(), 4);
1509 }
1510
1511 #[test]
1512 fn error_category_copy_clone_hash() {
1513 use std::collections::HashSet;
1514 let cat = ErrorCategory::Transport;
1515 let copied = cat;
1516 let cloned = cat;
1517 assert_eq!(copied, cloned);
1518
1519 let mut set = HashSet::new();
1520 set.insert(ErrorCategory::Cancellation);
1521 set.insert(ErrorCategory::Budget);
1522 set.insert(ErrorCategory::Channel);
1523 assert_eq!(set.len(), 3);
1524 }
1525
1526 #[test]
1527 fn backoff_hint_copy_hash_eq() {
1528 use std::collections::HashSet;
1529 let hint = BackoffHint::DEFAULT;
1530 let copied = hint;
1531 assert_eq!(copied, BackoffHint::DEFAULT);
1532 assert_ne!(hint, BackoffHint::AGGRESSIVE);
1533
1534 let mut set = HashSet::new();
1535 set.insert(BackoffHint::DEFAULT);
1536 set.insert(BackoffHint::AGGRESSIVE);
1537 set.insert(BackoffHint::QUICK);
1538 assert_eq!(set.len(), 3);
1539 }
1540
1541 #[test]
1542 fn recv_error_debug_clone_copy() {
1543 let err = RecvError::Disconnected;
1544 let dbg = format!("{err:?}");
1545 assert!(dbg.contains("Disconnected"));
1546
1547 let copied = err;
1548 assert_eq!(copied, RecvError::Disconnected);
1549
1550 let cloned = err;
1551 assert_eq!(cloned, err);
1552 }
1553
1554 #[test]
1555 fn cancelled_clone_eq() {
1556 let c = Cancelled {
1557 reason: CancelReason::user("test"),
1558 };
1559 let dbg = format!("{c:?}");
1560 assert!(dbg.contains("Cancelled"));
1561
1562 let cloned = c.clone();
1563 assert_eq!(cloned, c);
1564 }
1565
1566 #[test]
1567 fn error_context_auto_correlation() {
1568 let ctx = ErrorContext::new();
1569 assert!(ctx.correlation_id.is_some());
1570 assert!(ctx.causal_chain.is_empty());
1571 assert!(ctx.async_stack.is_empty());
1572 }
1573
1574 #[test]
1575 fn error_context_derive_child() {
1576 let parent = ErrorContext::new();
1577 let parent_id = parent.correlation_id.unwrap();
1578
1579 let child = parent.derive_child("async_operation");
1580
1581 assert!(child.correlation_id.is_some());
1583 assert_ne!(child.correlation_id, parent.correlation_id);
1584
1585 assert_eq!(child.causal_chain, vec![parent_id]);
1587
1588 assert_eq!(child.async_stack, vec!["async_operation"]);
1590
1591 assert!(child.span_id.is_some());
1593 assert_eq!(child.parent_span_id, parent.span_id);
1594 }
1595
1596 #[test]
1597 fn error_context_causal_chain() {
1598 let root = ErrorContext::new();
1599 let child = root.derive_child("level1");
1600 let grandchild = child.derive_child("level2");
1601
1602 let root_id = root.correlation_id.unwrap();
1603 let child_id = child.correlation_id.unwrap();
1604
1605 let chain = grandchild.full_causal_chain();
1606 assert_eq!(
1607 chain,
1608 vec![root_id, child_id, grandchild.correlation_id.unwrap()]
1609 );
1610
1611 assert_eq!(grandchild.root_correlation_id(), Some(root_id));
1612 }
1613
1614 #[test]
1615 fn error_context_async_stack_trace() {
1616 let ctx = ErrorContext::new()
1617 .with_operation("spawn_task")
1618 .with_operation("process_request");
1619
1620 let trace = ctx.format_async_stack();
1621 assert_eq!(trace, "spawn_task -> process_request");
1622 }
1623
1624 #[test]
1625 fn error_propagate_across_async() {
1626 let error = Error::new(ErrorKind::Internal).with_operation("initial_operation");
1627
1628 let propagated = error.propagate_across_async("async_boundary");
1629
1630 let chain = propagated.causal_chain();
1632 assert!(!chain.is_empty());
1633
1634 let stack = propagated.async_stack();
1636 assert!(stack.contains("async_boundary"));
1637 }
1638
1639 #[test]
1640 fn error_correlation_tracking() {
1641 let err1 = Error::new(ErrorKind::ChannelClosed);
1642 let err2 = Error::new(ErrorKind::Internal);
1643
1644 assert_ne!(err1.correlation_id(), err2.correlation_id());
1646 assert!(err1.correlation_id().is_some());
1647 assert!(err2.correlation_id().is_some());
1648 }
1649
1650 #[test]
1651 fn error_with_operations() {
1652 let error = Error::new(ErrorKind::DecodingFailed)
1653 .with_operation("read_symbol")
1654 .with_operation("decode_block");
1655
1656 let stack = error.async_stack();
1657 assert_eq!(stack, "read_symbol -> decode_block");
1658 }
1659}