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