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}