Skip to main content

qubit_cas/error/
cas_error.rs

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