Skip to main content

qubit_cas/error/
cas_error.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Terminal CAS errors.
10
11use std::error::Error;
12use std::fmt;
13use std::sync::Arc;
14
15use qubit_retry::{AttemptFailure, RetryError, RetryErrorReason};
16
17use crate::event::CasContext;
18
19use super::CasAttemptFailure;
20
21/// Classified reason for a terminal CAS error.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum CasErrorKind {
24    /// The operation explicitly aborted.
25    Abort,
26    /// Retry limits were exhausted by compare-and-swap conflicts.
27    Conflict,
28    /// Retry limits were exhausted by retryable business failures.
29    RetryExhausted,
30    /// A timeout aborted the flow or exhausted retry limits.
31    AttemptTimeout,
32    /// The total elapsed-time budget expired.
33    MaxElapsedExceeded,
34}
35
36/// Terminal CAS error returned by [`crate::CasExecutor`].
37#[derive(Clone)]
38pub struct CasError<T, E> {
39    /// Cached high-level CAS error kind.
40    kind: CasErrorKind,
41    /// Terminal reason selected by the retry layer.
42    reason: RetryErrorReason,
43    /// Copied CAS context captured when execution stopped.
44    context: CasContext,
45    /// Last attempt-level CAS failure, when one exists.
46    last_failure: Option<CasAttemptFailure<T, E>>,
47}
48
49impl<T, E> CasError<T, E> {
50    /// Wraps one retry-layer error.
51    ///
52    /// # Parameters
53    /// - `inner`: Retry-layer error to wrap.
54    /// - `attempt_timeout`: Optional timeout configured by the executor.
55    ///
56    /// # Returns
57    /// A [`CasError`] wrapper.
58    #[inline]
59    pub(crate) fn new(
60        inner: RetryError<CasAttemptFailure<T, E>>,
61        attempt_timeout: Option<std::time::Duration>,
62    ) -> Self {
63        let (reason, raw_last_failure, retry_context) = inner.into_parts();
64        let context = CasContext::new(&retry_context, attempt_timeout);
65        let last_failure = match raw_last_failure {
66            Some(AttemptFailure::Error(failure)) => Some(failure),
67            Some(AttemptFailure::Timeout)
68            | Some(AttemptFailure::Panic(_))
69            | Some(AttemptFailure::Executor(_))
70            | None => None,
71        };
72        let kind = Self::classify_kind(reason, last_failure.as_ref());
73        Self {
74            kind,
75            reason,
76            context,
77            last_failure,
78        }
79    }
80
81    /// Classifies one terminal CAS error kind from retry reason and failure.
82    ///
83    /// # Parameters
84    /// - `reason`: Terminal reason selected by the retry layer.
85    /// - `last_failure`: Last CAS failure when one exists.
86    ///
87    /// # Returns
88    /// Derived high-level CAS error kind.
89    fn classify_kind(
90        reason: RetryErrorReason,
91        last_failure: Option<&CasAttemptFailure<T, E>>,
92    ) -> CasErrorKind {
93        match reason {
94            RetryErrorReason::Aborted => match last_failure {
95                Some(CasAttemptFailure::Timeout { .. }) => CasErrorKind::AttemptTimeout,
96                _ => CasErrorKind::Abort,
97            },
98            RetryErrorReason::AttemptsExceeded
99            | RetryErrorReason::UnsupportedOperation
100            | RetryErrorReason::WorkerStillRunning => match last_failure {
101                Some(CasAttemptFailure::Conflict { .. }) => CasErrorKind::Conflict,
102                Some(CasAttemptFailure::Timeout { .. }) => CasErrorKind::AttemptTimeout,
103                _ => CasErrorKind::RetryExhausted,
104            },
105            RetryErrorReason::MaxElapsedExceeded => CasErrorKind::MaxElapsedExceeded,
106        }
107    }
108
109    /// Returns the classified CAS error kind.
110    ///
111    /// # Returns
112    /// High-level CAS error kind derived from the retry-layer reason and last
113    /// attempt failure.
114    #[inline]
115    pub fn kind(&self) -> CasErrorKind {
116        self.kind
117    }
118
119    /// Returns the retry-layer terminal reason.
120    ///
121    /// # Returns
122    /// Underlying [`RetryErrorReason`].
123    #[inline]
124    pub fn reason(&self) -> RetryErrorReason {
125        self.reason
126    }
127
128    /// Returns the terminal CAS context.
129    ///
130    /// # Returns
131    /// Copied CAS context captured when execution stopped.
132    #[inline]
133    pub fn context(&self) -> CasContext {
134        self.context
135    }
136
137    /// Returns the number of attempts that were executed.
138    ///
139    /// # Returns
140    /// One-based attempt count.
141    #[inline]
142    pub fn attempts(&self) -> u32 {
143        self.context.attempt()
144    }
145
146    /// Returns the last CAS attempt failure when one exists.
147    ///
148    /// # Returns
149    /// `Some(&CasAttemptFailure<T, E>)` when at least one attempt failed.
150    #[inline]
151    pub fn last_failure(&self) -> Option<&CasAttemptFailure<T, E>> {
152        self.last_failure.as_ref()
153    }
154
155    /// Returns the current state associated with the last failure.
156    ///
157    /// # Returns
158    /// `Some(&Arc<T>)` when the terminal error preserved a current state.
159    #[inline]
160    pub fn current(&self) -> Option<&Arc<T>> {
161        self.last_failure().map(CasAttemptFailure::current)
162    }
163
164    /// Returns the business error associated with the last failure.
165    ///
166    /// # Returns
167    /// `Some(&E)` for retryable or aborting business failures.
168    #[inline]
169    pub fn error(&self) -> Option<&E> {
170        self.last_failure().and_then(CasAttemptFailure::error)
171    }
172}
173
174impl<T, E> fmt::Debug for CasError<T, E> {
175    /// Formats the CAS error for debugging without requiring `T: Debug`.
176    ///
177    /// # Parameters
178    /// - `f`: Formatter provided by the standard formatting machinery.
179    ///
180    /// # Returns
181    /// `fmt::Result` from the formatter.
182    ///
183    /// # Errors
184    /// Returns a formatting error if the formatter fails.
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        f.debug_struct("CasError")
187            .field("kind", &self.kind())
188            .field("reason", &self.reason())
189            .field("context", &self.context())
190            .finish()
191    }
192}
193
194impl<T, E> fmt::Display for CasError<T, E>
195where
196    E: fmt::Display,
197{
198    /// Formats the terminal CAS error.
199    ///
200    /// # Parameters
201    /// - `f`: Formatter provided by the standard formatting machinery.
202    ///
203    /// # Returns
204    /// `fmt::Result` from the formatter.
205    ///
206    /// # Errors
207    /// Returns a formatting error if the formatter fails.
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        let message = match self.kind() {
210            CasErrorKind::Abort => "CAS aborted",
211            CasErrorKind::Conflict => "CAS conflicts exhausted",
212            CasErrorKind::RetryExhausted => "CAS retryable failures exhausted",
213            CasErrorKind::AttemptTimeout => "CAS attempt timed out",
214            CasErrorKind::MaxElapsedExceeded => "CAS max elapsed exceeded",
215        };
216        write!(f, "{message} after {} attempt(s)", self.attempts())?;
217        if let Some(failure) = self.last_failure() {
218            write!(f, "; last failure: {failure}")?;
219        }
220        Ok(())
221    }
222}
223
224impl<T, E> Error for CasError<T, E>
225where
226    E: Error + 'static,
227{
228    /// Returns the source business error when one exists.
229    ///
230    /// # Returns
231    /// `Some(&dyn Error)` when the terminal CAS failure preserved a business
232    /// error implementing [`std::error::Error`].
233    fn source(&self) -> Option<&(dyn Error + 'static)> {
234        self.error().map(|error| error as &(dyn Error + 'static))
235    }
236}