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