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 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}