qubit_retry/error/retry_error.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! Retry execution errors.
10//!
11//! This module contains the error returned when a retry executor stops without a
12//! successful result. The original application error type is preserved in the
13//! generic parameter `E`.
14
15use std::error::Error;
16use std::fmt;
17use std::time::Duration;
18
19use super::RetryAttemptFailure;
20
21/// Error returned when a retry executor terminates without a successful result.
22///
23/// The generic parameter `E` is the caller's application error type. It is
24/// preserved in the final [`RetryAttemptFailure`] whenever the terminal failure came
25/// from the user operation.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum RetryError<E> {
28 /// The configured classifier marked the last failure as non-retryable.
29 Aborted {
30 /// Number of attempts that were executed.
31 attempts: u32,
32 /// Total elapsed time observed by the retry executor.
33 elapsed: Duration,
34 /// Failure that caused the abort.
35 failure: RetryAttemptFailure<E>,
36 },
37
38 /// The maximum number of attempts has been exhausted.
39 AttemptsExceeded {
40 /// Number of attempts that were executed.
41 attempts: u32,
42 /// Configured attempt limit.
43 max_attempts: u32,
44 /// Total elapsed time observed by the retry executor.
45 elapsed: Duration,
46 /// Last observed failure.
47 last_failure: RetryAttemptFailure<E>,
48 },
49
50 /// The total elapsed retry budget has been exhausted.
51 MaxElapsedExceeded {
52 /// Number of attempts that were executed.
53 attempts: u32,
54 /// Total elapsed time observed by the retry executor.
55 elapsed: Duration,
56 /// Configured elapsed budget.
57 max_elapsed: Duration,
58 /// Last failure, if any attempt ran before the budget was exhausted.
59 last_failure: Option<RetryAttemptFailure<E>>,
60 },
61}
62
63impl<E> RetryError<E> {
64 /// Returns the number of attempts that were executed.
65 ///
66 /// # Parameters
67 /// This method has no parameters.
68 ///
69 /// # Returns
70 /// The number of operation attempts observed before termination.
71 ///
72 /// # Errors
73 /// This method does not return errors.
74 #[inline]
75 pub fn attempts(&self) -> u32 {
76 match self {
77 Self::Aborted { attempts, .. }
78 | Self::AttemptsExceeded { attempts, .. }
79 | Self::MaxElapsedExceeded { attempts, .. } => *attempts,
80 }
81 }
82
83 /// Returns the elapsed time recorded at termination.
84 ///
85 /// # Parameters
86 /// This method has no parameters.
87 ///
88 /// # Returns
89 /// The elapsed duration recorded by the retry executor when it stopped.
90 ///
91 /// # Errors
92 /// This method does not return errors.
93 #[inline]
94 pub fn elapsed(&self) -> Duration {
95 match self {
96 Self::Aborted { elapsed, .. }
97 | Self::AttemptsExceeded { elapsed, .. }
98 | Self::MaxElapsedExceeded { elapsed, .. } => *elapsed,
99 }
100 }
101
102 /// Returns the last failure, if one exists.
103 ///
104 /// # Parameters
105 /// This method has no parameters.
106 ///
107 /// # Returns
108 /// `Some(&RetryAttemptFailure<E>)` when at least one attempt failure was
109 /// observed; `None` when the elapsed budget was exhausted before any
110 /// attempt ran.
111 ///
112 /// # Errors
113 /// This method does not return errors.
114 #[inline]
115 pub fn last_failure(&self) -> Option<&RetryAttemptFailure<E>> {
116 match self {
117 Self::Aborted { failure, .. } => Some(failure),
118 Self::AttemptsExceeded { last_failure, .. } => Some(last_failure),
119 Self::MaxElapsedExceeded { last_failure, .. } => last_failure.as_ref(),
120 }
121 }
122
123 /// Returns the last application error, if one exists.
124 ///
125 /// # Parameters
126 /// This method has no parameters.
127 ///
128 /// # Returns
129 /// `Some(&E)` when the terminal failure wraps an application error;
130 /// `None` for timeout failures or elapsed-budget failures with no attempt.
131 ///
132 /// # Errors
133 /// This method does not return errors.
134 #[inline]
135 pub fn last_error(&self) -> Option<&E> {
136 self.last_failure().and_then(RetryAttemptFailure::as_error)
137 }
138
139 /// Consumes the retry error and returns the last application error when
140 /// the final failure wraps one.
141 ///
142 /// # Parameters
143 /// This method has no parameters.
144 ///
145 /// # Returns
146 /// `Some(E)` when the terminal failure owns an application error; `None`
147 /// when the terminal failure was a timeout or when no attempt ran.
148 ///
149 /// # Errors
150 /// This method does not return errors.
151 #[inline]
152 pub fn into_last_error(self) -> Option<E> {
153 match self {
154 Self::Aborted { failure, .. } => failure.into_error(),
155 Self::AttemptsExceeded { last_failure, .. } => last_failure.into_error(),
156 Self::MaxElapsedExceeded { last_failure, .. } => {
157 last_failure.and_then(RetryAttemptFailure::into_error)
158 }
159 }
160 }
161}
162
163impl<E> fmt::Display for RetryError<E>
164where
165 E: fmt::Display,
166{
167 /// Formats the retry error for diagnostics.
168 ///
169 /// # Parameters
170 /// - `f`: Formatter provided by the standard formatting machinery.
171 ///
172 /// # Returns
173 /// `fmt::Result` from the formatter.
174 ///
175 /// # Errors
176 /// Returns a formatting error if the underlying formatter fails.
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match self {
179 Self::Aborted {
180 attempts,
181 failure,
182 ..
183 } => write!(
184 f,
185 "retry aborted after {attempts} attempt(s); failure: {failure}"
186 ),
187 Self::AttemptsExceeded {
188 attempts,
189 max_attempts,
190 last_failure,
191 ..
192 } => write!(
193 f,
194 "retry attempts exceeded: {attempts} attempt(s), max {max_attempts}; last failure: {last_failure}"
195 ),
196 Self::MaxElapsedExceeded {
197 attempts,
198 max_elapsed,
199 last_failure,
200 ..
201 } => {
202 if let Some(failure) = last_failure {
203 write!(
204 f,
205 "retry max elapsed exceeded after {attempts} attempt(s); max {max_elapsed:?}; last failure: {failure}"
206 )
207 } else {
208 write!(
209 f,
210 "retry max elapsed exceeded after {attempts} attempt(s); max {max_elapsed:?}"
211 )
212 }
213 }
214 }
215 }
216}
217
218impl<E> Error for RetryError<E>
219where
220 E: Error + 'static,
221{
222 /// Returns the source application error when one is available.
223 ///
224 /// # Parameters
225 /// This method has no parameters.
226 ///
227 /// # Returns
228 /// `Some(&dyn Error)` when the terminal failure wraps an application error
229 /// that implements [`std::error::Error`]; otherwise `None`.
230 ///
231 /// # Errors
232 /// This method does not return errors.
233 fn source(&self) -> Option<&(dyn Error + 'static)> {
234 self.last_error()
235 .map(|error| error as &(dyn Error + 'static))
236 }
237}