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}