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