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