Skip to main content

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}