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