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}