Skip to main content

asupersync/
error.rs

1//! Error types and error handling strategy for Asupersync.
2//!
3//! This module defines the core error types used throughout the runtime.
4//! Error handling follows these principles:
5//!
6//! - Errors are explicit and typed (no stringly-typed errors)
7//! - Errors compose well with the Outcome severity lattice
8//! - Panics are isolated and converted to `Outcome::Panicked`
9//! - Errors are classified by recoverability for retry logic
10//!
11//! # Error Categories
12//!
13//! Errors are organized into categories:
14//!
15//! - **Cancellation**: Operation cancelled by request or timeout
16//! - **Budgets**: Resource limits exceeded (deadlines, quotas)
17//! - **Channels**: Communication primitive errors
18//! - **Obligations**: Linear resource tracking violations
19//! - **Regions**: Ownership and lifecycle errors
20//! - **Encoding**: RaptorQ encoding pipeline errors
21//! - **Decoding**: RaptorQ decoding pipeline errors
22//! - **Transport**: Symbol routing and transmission errors
23//! - **Distributed**: Distributed region coordination errors
24//! - **Internal**: Runtime bugs and invalid states
25//!
26//! # Recovery Classification
27//!
28//! All errors can be classified by [`Recoverability`]:
29//! - `Transient`: Temporary failure, safe to retry
30//! - `Permanent`: Unrecoverable, do not retry
31//! - `Unknown`: Recoverability depends on context
32
33use 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/// The kind of error.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum ErrorKind {
47    // === Cancellation ===
48    /// Operation was cancelled.
49    Cancelled,
50    /// Cancellation cleanup budget was exceeded.
51    CancelTimeout,
52
53    // === Budgets ===
54    /// Deadline exceeded.
55    DeadlineExceeded,
56    /// Poll quota exhausted.
57    PollQuotaExhausted,
58    /// Cost quota exhausted.
59    CostQuotaExhausted,
60
61    // === Channels ===
62    /// Channel is closed/disconnected.
63    ChannelClosed,
64    /// Channel is full (would block).
65    ChannelFull,
66    /// Channel is empty (would block).
67    ChannelEmpty,
68
69    // === Obligations ===
70    /// Obligation was not resolved before close/completion.
71    ObligationLeak,
72    /// Tried to resolve an already-resolved obligation.
73    ObligationAlreadyResolved,
74    /// Tried to resolve an obligation after its region was finalized.
75    RegionFinalized,
76
77    // === Regions / ownership ===
78    /// Region is already closed.
79    RegionClosed,
80    /// Task not owned by region.
81    TaskNotOwned,
82    /// Region admission/backpressure limit reached.
83    AdmissionDenied,
84
85    // === Encoding (RaptorQ) ===
86    /// Invalid encoding parameters (symbol size, block count, etc.).
87    InvalidEncodingParams,
88    /// Source data too large for configured parameters.
89    DataTooLarge,
90    /// Encoding operation failed.
91    EncodingFailed,
92    /// Symbol data is corrupted or invalid.
93    CorruptedSymbol,
94
95    // === Decoding (RaptorQ) ===
96    /// Not enough symbols received to decode.
97    InsufficientSymbols,
98    /// Decoding operation failed (matrix singular, etc.).
99    DecodingFailed,
100    /// Symbol does not belong to the expected object.
101    ObjectMismatch,
102    /// Received duplicate symbol.
103    DuplicateSymbol,
104    /// Decoding threshold not met within timeout.
105    ThresholdTimeout,
106
107    // === Transport ===
108    /// Symbol routing failed (no route to destination).
109    RoutingFailed,
110    /// Symbol dispatch failed.
111    DispatchFailed,
112    /// Symbol stream ended unexpectedly.
113    StreamEnded,
114    /// Symbol sink rejected the symbol.
115    SinkRejected,
116    /// Transport connection lost.
117    ConnectionLost,
118    /// Transport connection refused.
119    ConnectionRefused,
120    /// Transport protocol error.
121    ProtocolError,
122    /// Request rate limited by the remote endpoint.
123    RateLimited,
124    /// Invalid input provided to operation.
125    InvalidInput,
126    /// Operation failed to complete successfully.
127    OperationFailed,
128
129    // === Distributed Regions ===
130    /// Region recovery failed.
131    RecoveryFailed,
132    /// Lease expired during operation.
133    LeaseExpired,
134    /// Lease renewal failed.
135    LeaseRenewalFailed,
136    /// Distributed coordination failed.
137    CoordinationFailed,
138    /// Quorum not reached.
139    QuorumNotReached,
140    /// Node is unavailable.
141    NodeUnavailable,
142    /// Partition detected (split brain).
143    PartitionDetected,
144
145    // === Internal / state machine ===
146    /// Internal runtime error (bug).
147    Internal,
148    /// Invalid state transition.
149    InvalidStateTransition,
150
151    // === Configuration ===
152    /// Configuration error (invalid env var, bad config file, etc.).
153    ConfigError,
154
155    // === User ===
156    /// User-provided error.
157    User,
158}
159
160impl ErrorKind {
161    /// Returns the stable ASUP error code for user-facing diagnostics.
162    #[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    /// Returns the error category for this kind.
178    #[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    /// Returns the recoverability classification for this error kind.
225    ///
226    /// This helps retry logic decide whether to attempt recovery.
227    #[must_use]
228    #[inline]
229    pub const fn recoverability(&self) -> Recoverability {
230        match self {
231            // Transient errors - safe to retry
232            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            // Permanent errors - do not retry
243            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            // Context-dependent errors
261            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    /// Returns true if this error is typically retryable.
284    #[must_use]
285    #[inline]
286    pub const fn is_retryable(&self) -> bool {
287        matches!(self.recoverability(), Recoverability::Transient)
288    }
289
290    /// Returns the recommended recovery action for this error kind.
291    ///
292    /// This provides more specific guidance than [`recoverability()`](Self::recoverability)
293    /// about how to handle the error.
294    #[must_use]
295    #[inline]
296    pub const fn recovery_action(&self) -> RecoveryAction {
297        match self {
298            // Immediate retry - brief transient states
299            Self::ChannelFull | Self::ChannelEmpty => RecoveryAction::RetryImmediately,
300
301            // Backoff retry - transient but may need time to clear
302            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            // Reconnect - connection is likely broken
310            Self::ConnectionLost | Self::StreamEnded => RecoveryAction::RetryWithNewConnection,
311
312            // Propagate - let caller decide
313            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            // Escalate - serious problem, should cancel related work
332            Self::ObligationLeak
333            | Self::ObligationAlreadyResolved
334            | Self::RegionFinalized
335            | Self::Internal
336            | Self::InvalidStateTransition => RecoveryAction::Escalate,
337
338            // Custom - depends on application context
339            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/// Classification of error recoverability for retry logic.
356///
357/// This enum helps the retry combinator and error handling code
358/// decide how to handle failures.
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
360pub enum Recoverability {
361    /// Temporary failure that may succeed on retry.
362    Transient,
363    /// Permanent failure that will not succeed on retry.
364    Permanent,
365    /// Recoverability depends on context and cannot be determined
366    /// from the error kind alone.
367    Unknown,
368}
369
370/// Recommended recovery action for an error.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
372pub enum RecoveryAction {
373    /// Retry the operation immediately.
374    RetryImmediately,
375    /// Retry the operation with exponential backoff.
376    RetryWithBackoff(BackoffHint),
377    /// Retry after establishing a new connection.
378    RetryWithNewConnection,
379    /// Propagate the error to the caller without retry.
380    Propagate,
381    /// Escalate by requesting cancellation of the current operation tree.
382    Escalate,
383    /// Recovery action depends on application-specific context.
384    Custom,
385}
386
387/// Hints for configuring exponential backoff.
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
389pub struct BackoffHint {
390    /// Suggested initial delay before first retry.
391    pub initial_delay_ms: u32,
392    /// Suggested maximum delay between retries.
393    pub max_delay_ms: u32,
394    /// Suggested maximum number of retry attempts.
395    pub max_attempts: u8,
396}
397
398impl BackoffHint {
399    /// Default backoff hint for transient errors.
400    pub const DEFAULT: Self = Self {
401        initial_delay_ms: 100,
402        max_delay_ms: 30_000,
403        max_attempts: 5,
404    };
405
406    /// Aggressive backoff for rate-limiting or overload scenarios.
407    pub const AGGRESSIVE: Self = Self {
408        initial_delay_ms: 1_000,
409        max_delay_ms: 60_000,
410        max_attempts: 10,
411    };
412
413    /// Quick backoff for brief transient failures.
414    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    /// Returns true if this error is safe to retry.
430    #[must_use]
431    #[inline]
432    pub const fn should_retry(&self) -> bool {
433        matches!(self, Self::Transient)
434    }
435
436    /// Returns true if this error should never be retried.
437    #[must_use]
438    #[inline]
439    pub const fn is_permanent(&self) -> bool {
440        matches!(self, Self::Permanent)
441    }
442}
443
444/// High-level error category for grouping related errors.
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
446pub enum ErrorCategory {
447    /// Cancellation-related failures.
448    Cancellation,
449    /// Budget/time/resource limit failures.
450    Budget,
451    /// Channel and messaging failures.
452    Channel,
453    /// Obligation lifecycle failures.
454    Obligation,
455    /// Region lifecycle failures.
456    Region,
457    /// Encoding failures.
458    Encoding,
459    /// Decoding failures.
460    Decoding,
461    /// Transport-layer failures.
462    Transport,
463    /// Distributed runtime failures.
464    Distributed,
465    /// Internal runtime errors.
466    Internal,
467    /// User-originated errors.
468    User,
469}
470
471/// Diagnostic context for an error.
472#[derive(Debug, Clone, Default, PartialEq, Eq)]
473pub struct ErrorContext {
474    /// The task where the error originated.
475    pub task_id: Option<TaskId>,
476    /// The region owning the task.
477    pub region_id: Option<RegionId>,
478    /// The object involved in the error (for distributed operations).
479    pub object_id: Option<ObjectId>,
480    /// The symbol involved in the error (for RaptorQ).
481    pub symbol_id: Option<SymbolId>,
482    /// Correlation ID for tracing error propagation across async boundaries.
483    pub correlation_id: Option<u64>,
484    /// Parent correlation IDs forming a causal chain.
485    pub causal_chain: Vec<u64>,
486    /// Span ID from the current tracing context.
487    pub span_id: Option<crate::observability::SpanId>,
488    /// Parent span ID for building async stack traces.
489    pub parent_span_id: Option<crate::observability::SpanId>,
490    /// Async stack trace showing error propagation path.
491    pub async_stack: Vec<String>,
492}
493
494impl ErrorContext {
495    /// Creates a new error context with automatic correlation ID generation.
496    #[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    /// Creates an error context from the current Cx diagnostic context.
506    #[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    /// Derives a child error context preserving causal chain.
517    #[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()), // New span for child operation
538            parent_span_id: self.span_id,
539            async_stack,
540        }
541    }
542
543    /// Adds an operation to the async stack trace.
544    #[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    /// Sets the span context from current tracing.
551    #[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    /// Returns the root correlation ID from the causal chain.
559    #[must_use]
560    pub fn root_correlation_id(&self) -> Option<u64> {
561        self.causal_chain.first().copied().or(self.correlation_id)
562    }
563
564    /// Returns the full causal chain including current correlation ID.
565    #[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    /// Returns a human-readable async stack trace.
575    #[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/// The main error type for Asupersync operations.
586#[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    /// Creates a new error with the given kind.
596    #[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    /// Returns the error kind.
608    #[must_use]
609    #[inline]
610    pub const fn kind(&self) -> ErrorKind {
611        self.kind
612    }
613
614    /// Returns true if this error represents cancellation.
615    #[must_use]
616    #[inline]
617    pub const fn is_cancelled(&self) -> bool {
618        matches!(self.kind, ErrorKind::Cancelled)
619    }
620
621    /// Returns true if this error is a timeout/deadline condition.
622    #[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    /// Adds a message description to the error.
632    #[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    /// Adds structured context to the error.
640    #[must_use]
641    #[inline]
642    pub fn with_context(mut self, ctx: ErrorContext) -> Self {
643        self.context = ctx;
644        self
645    }
646
647    /// Adds a source error to the chain.
648    #[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    /// Creates an error with context derived from current Cx.
656    #[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    /// Propagates an error across an async boundary, preserving causal chain.
666    #[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    /// Adds an operation to the error's async stack trace.
673    #[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    /// Returns the correlation ID for tracing this error.
680    #[must_use]
681    #[inline]
682    pub fn correlation_id(&self) -> Option<u64> {
683        self.context.correlation_id
684    }
685
686    /// Returns the root cause correlation ID.
687    #[must_use]
688    #[inline]
689    pub fn root_correlation_id(&self) -> Option<u64> {
690        self.context.root_correlation_id()
691    }
692
693    /// Returns the full causal chain for root cause analysis.
694    #[must_use]
695    #[inline]
696    pub fn causal_chain(&self) -> Vec<u64> {
697        self.context.full_causal_chain()
698    }
699
700    /// Returns a formatted async stack trace.
701    #[must_use]
702    #[inline]
703    pub fn async_stack(&self) -> String {
704        self.context.format_async_stack()
705    }
706
707    /// Creates a cancellation error from a structured reason.
708    #[must_use]
709    #[inline]
710    pub fn cancelled(reason: &CancelReason) -> Self {
711        Self::new(ErrorKind::Cancelled).with_message(reason.to_string())
712    }
713
714    /// Returns the error category.
715    #[must_use]
716    #[inline]
717    pub const fn category(&self) -> ErrorCategory {
718        self.kind.category()
719    }
720
721    /// Returns the recoverability classification.
722    #[must_use]
723    #[inline]
724    pub const fn recoverability(&self) -> Recoverability {
725        self.kind.recoverability()
726    }
727
728    /// Returns true if this error is typically retryable.
729    #[must_use]
730    #[inline]
731    pub const fn is_retryable(&self) -> bool {
732        self.kind.is_retryable()
733    }
734
735    /// Returns the recommended recovery action for this error.
736    #[must_use]
737    #[inline]
738    pub const fn recovery_action(&self) -> RecoveryAction {
739        self.kind.recovery_action()
740    }
741
742    /// Returns the error message, if any.
743    #[must_use]
744    #[inline]
745    pub fn message(&self) -> Option<&str> {
746        self.message.as_deref()
747    }
748
749    /// Returns the error context.
750    #[must_use]
751    #[inline]
752    pub fn context(&self) -> &ErrorContext {
753        &self.context
754    }
755
756    /// Returns true if this is an encoding-related error.
757    #[must_use]
758    #[inline]
759    pub const fn is_encoding_error(&self) -> bool {
760        matches!(self.kind.category(), ErrorCategory::Encoding)
761    }
762
763    /// Returns true if this is a decoding-related error.
764    #[must_use]
765    #[inline]
766    pub const fn is_decoding_error(&self) -> bool {
767        matches!(self.kind.category(), ErrorCategory::Decoding)
768    }
769
770    /// Returns true if this is a transport-related error.
771    #[must_use]
772    #[inline]
773    pub const fn is_transport_error(&self) -> bool {
774        matches!(self.kind.category(), ErrorCategory::Transport)
775    }
776
777    /// Returns true if this is a distributed coordination error.
778    #[must_use]
779    #[inline]
780    pub const fn is_distributed_error(&self) -> bool {
781        matches!(self.kind.category(), ErrorCategory::Distributed)
782    }
783
784    /// Returns true if this is a connection-related error.
785    #[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    /// Creates an encoding error with parameters context.
795    #[must_use]
796    pub fn invalid_encoding_params(detail: impl Into<String>) -> Self {
797        Self::new(ErrorKind::InvalidEncodingParams).with_message(detail)
798    }
799
800    /// Creates a data too large error.
801    #[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    /// Creates an insufficient symbols error for decoding.
808    #[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    /// Creates a decoding failed error.
816    #[must_use]
817    pub fn decoding_failed(reason: impl Into<String>) -> Self {
818        Self::new(ErrorKind::DecodingFailed).with_message(reason)
819    }
820
821    /// Creates a routing failed error.
822    #[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    /// Creates a lease expired error.
829    #[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    /// Creates a quorum not reached error.
836    #[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    /// Creates a node unavailable error.
843    #[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    /// Creates an internal error (runtime bug).
850    #[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/// Marker type for cancellation, carrying a reason.
876#[derive(Debug, Clone, PartialEq, Eq)]
877pub struct Cancelled {
878    /// The reason for cancellation.
879    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/// Error when sending on a channel.
889#[derive(Debug)]
890pub enum SendError<T> {
891    /// Channel receiver was dropped.
892    Disconnected(T),
893    /// Would block (bounded channel is full).
894    Full(T),
895    /// The send operation was cancelled.
896    Cancelled(T),
897}
898
899/// Error when receiving from a channel.
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub enum RecvError {
902    /// Channel sender was dropped.
903    Disconnected,
904    /// Would block (channel empty).
905    Empty,
906    /// The receive operation was cancelled.
907    Cancelled,
908}
909
910/// Error when acquiring a semaphore-like permit.
911#[derive(Debug, Clone, Copy, PartialEq, Eq)]
912pub enum AcquireError {
913    /// Semaphore/permit source closed.
914    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/// Extension trait for adding context to Results.
949#[allow(clippy::result_large_err)]
950pub trait ResultExt<T> {
951    /// Attach a context message on error.
952    fn context(self, msg: impl Into<String>) -> Result<T>;
953    /// Attach context message computed lazily on error.
954    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/// A specialized Result type for Asupersync operations.
968#[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    // ---- ErrorKind category exhaustive coverage ----
1143
1144    #[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        // Transient
1197        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        // Permanent
1212        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        // Unknown
1225        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    // ---- Error convenience constructors ----
1299
1300    #[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    // ---- Error predicates ----
1356
1357    #[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    // ---- Conversion tests ----
1383
1384    #[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    // ---- Debug/Clone ----
1416
1417    #[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    // Pure data-type tests (wave 39 – CyanBarn)
1466
1467    #[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); // duplicate
1478        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        // Child has new correlation ID
1582        assert!(child.correlation_id.is_some());
1583        assert_ne!(child.correlation_id, parent.correlation_id);
1584
1585        // Causal chain includes parent
1586        assert_eq!(child.causal_chain, vec![parent_id]);
1587
1588        // Operation added to stack
1589        assert_eq!(child.async_stack, vec!["async_operation"]);
1590
1591        // Spans are updated
1592        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        // Original error correlation should be in causal chain
1631        let chain = propagated.causal_chain();
1632        assert!(!chain.is_empty());
1633
1634        // Async stack should include new operation
1635        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        // Different errors get different correlation IDs
1645        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}