Skip to main content

qubit_retry/error/
retry_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//! Retry execution errors.
11//!
12//! This module contains the error returned when a retry executor stops without a
13//! successful result. The original application error type is preserved in the
14//! generic parameter `E`.
15
16use serde::{
17    Deserialize,
18    Serialize,
19};
20use std::error::Error;
21use std::fmt;
22
23use crate::{
24    AttemptFailure,
25    RetryContext,
26    RetryErrorReason,
27};
28
29/// Error returned when a retry flow terminates without a successful result.
30///
31/// The generic parameter `E` is the caller's application error type. It is
32/// preserved in [`AttemptFailure::Error`] when the terminal failure came from
33/// the user operation. Runtime failures such as timeout, panic, and executor
34/// failures are preserved through [`RetryError::last_failure`].
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(bound(
37    serialize = "E: serde::Serialize",
38    deserialize = "E: serde::de::DeserializeOwned"
39))]
40pub struct RetryError<E> {
41    /// Terminal reason selected by the retry flow.
42    reason: RetryErrorReason,
43    /// Last attempt failure, if any attempt ran before termination.
44    last_failure: Option<AttemptFailure<E>>,
45    /// Context snapshot captured when the retry flow stopped.
46    context: RetryContext,
47}
48
49/// Result alias returned by retry executor execution.
50///
51/// The success type `T` is chosen by each operation. The error type `E`
52/// remains the caller's original application error and is wrapped by
53/// [`RetryError`] only when retry execution terminates unsuccessfully.
54pub type RetryResult<T, E> = Result<T, RetryError<E>>;
55
56impl<E> RetryError<E> {
57    /// Creates a retry error.
58    ///
59    /// # Parameters
60    /// - `reason`: Terminal reason.
61    /// - `last_failure`: Last observed attempt failure, if any.
62    /// - `context`: Retry context captured at termination.
63    ///
64    /// # Returns
65    /// A retry error preserving the terminal reason and context.
66    #[inline]
67    pub(crate) fn new(
68        reason: RetryErrorReason,
69        last_failure: Option<AttemptFailure<E>>,
70        context: RetryContext,
71    ) -> Self {
72        Self {
73            reason,
74            last_failure,
75            context,
76        }
77    }
78
79    /// Returns the terminal retry error reason.
80    ///
81    /// # Parameters
82    /// This method has no parameters.
83    ///
84    /// # Returns
85    /// The reason the retry flow stopped.
86    #[inline]
87    pub fn reason(&self) -> RetryErrorReason {
88        self.reason
89    }
90
91    /// Returns the retry context captured at termination.
92    ///
93    /// # Parameters
94    /// This method has no parameters.
95    ///
96    /// # Returns
97    /// A context snapshot with attempt counts and timing metadata.
98    #[inline]
99    pub fn context(&self) -> &RetryContext {
100        &self.context
101    }
102
103    /// Returns the timeout source that produced the final attempt timeout, if any.
104    ///
105    /// # Parameters
106    /// This method has no parameters.
107    ///
108    /// # Returns
109    /// The timeout source when present, or `None` when no attempt timeout was
110    /// selected for the terminal context.
111    #[inline]
112    pub fn attempt_timeout_source(&self) -> Option<crate::event::AttemptTimeoutSource> {
113        self.context.attempt_timeout_source()
114    }
115
116    /// Returns the number of worker threads not observed to exit after cancellation.
117    ///
118    /// # Parameters
119    /// This method has no parameters.
120    ///
121    /// # Returns
122    /// Count of timed-out worker attempts that did not finish within the worker
123    /// cancellation grace period.
124    #[inline]
125    pub fn unreaped_worker_count(&self) -> u32 {
126        self.context.unreaped_worker_count()
127    }
128
129    /// Returns the number of attempts that were executed.
130    ///
131    /// # Returns
132    /// The number of operation attempts observed before termination.
133    #[inline]
134    pub fn attempts(&self) -> u32 {
135        self.context.attempt()
136    }
137
138    /// Returns the last failure, if one exists.
139    ///
140    /// # Returns
141    /// `Some(&AttemptFailure<E>)` when at least one attempt failure was observed;
142    /// `None` when the retry flow stopped before any attempt ran.
143    #[inline]
144    pub fn last_failure(&self) -> Option<&AttemptFailure<E>> {
145        self.last_failure.as_ref()
146    }
147
148    /// Returns the last application error, if one exists.
149    ///
150    /// # Parameters
151    /// This method has no parameters.
152    ///
153    /// # Returns
154    /// `Some(&E)` when the terminal failure wraps an application error;
155    /// `None` for timeout, panic, executor failures, or elapsed-budget failures
156    /// with no attempt.
157    #[inline]
158    pub fn last_error(&self) -> Option<&E> {
159        self.last_failure().and_then(AttemptFailure::as_error)
160    }
161
162    /// Consumes the retry error and returns the last application error when
163    /// the final failure wraps one.
164    ///
165    /// # Parameters
166    /// This method has no parameters.
167    ///
168    /// # Returns
169    /// `Some(E)` when the terminal failure owns an application error; `None`
170    /// when the terminal failure was a timeout, panic, executor failure, or
171    /// when no attempt ran.
172    #[inline]
173    pub fn into_last_error(self) -> Option<E> {
174        self.last_failure.and_then(AttemptFailure::into_error)
175    }
176
177    /// Consumes the retry error and returns all terminal parts.
178    ///
179    /// # Parameters
180    /// This method has no parameters.
181    ///
182    /// # Returns
183    /// A tuple `(reason, last_failure, context)` preserving all terminal data.
184    #[inline]
185    pub fn into_parts(self) -> (RetryErrorReason, Option<AttemptFailure<E>>, RetryContext) {
186        (self.reason, self.last_failure, self.context)
187    }
188}
189
190impl<E> fmt::Display for RetryError<E>
191where
192    E: fmt::Display,
193{
194    /// Formats the retry error for diagnostics.
195    ///
196    /// # Parameters
197    /// - `f`: Formatter provided by the standard formatting machinery.
198    ///
199    /// # Returns
200    /// `fmt::Result` from the formatter.
201    ///
202    /// # Errors
203    /// Returns a formatting error if the underlying formatter fails.
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        let attempts = self.attempts();
206        let message = match self.reason {
207            RetryErrorReason::Aborted => format!("retry aborted after {attempts} attempt(s)"),
208            RetryErrorReason::AttemptsExceeded => format!(
209                "retry attempts exceeded after {attempts} attempt(s), max {}",
210                self.context.max_attempts()
211            ),
212            RetryErrorReason::MaxOperationElapsedExceeded => {
213                format!("retry max operation elapsed exceeded after {attempts} attempt(s)")
214            }
215            RetryErrorReason::MaxTotalElapsedExceeded => {
216                format!("retry max total elapsed exceeded after {attempts} attempt(s)")
217            }
218            RetryErrorReason::UnsupportedOperation => {
219                "run() does not support attempt timeout; use run_async() or run_in_worker()"
220                    .to_string()
221            }
222            RetryErrorReason::WorkerStillRunning => {
223                format!(
224                    "retry worker still running after timeout cancellation grace, unreaped {}",
225                    self.context.unreaped_worker_count()
226                )
227            }
228        };
229        f.write_str(&message)?;
230        if let Some(failure) = &self.last_failure {
231            write!(f, "; last failure: {failure}")?;
232        }
233        Ok(())
234    }
235}
236
237impl<E> Error for RetryError<E>
238where
239    E: Error + 'static,
240{
241    /// Returns the source terminal failure when one is available.
242    ///
243    /// # Parameters
244    /// This method has no parameters.
245    ///
246    /// # Returns
247    /// `Some(&dyn Error)` when the terminal failure wraps an application error,
248    /// captured panic, or executor failure; otherwise `None`.
249    ///
250    /// # Errors
251    /// This method does not return errors.
252    fn source(&self) -> Option<&(dyn Error + 'static)> {
253        match self.last_failure() {
254            Some(AttemptFailure::Error(error)) => Some(error as &(dyn Error + 'static)),
255            Some(AttemptFailure::Panic(panic)) => Some(panic as &(dyn Error + 'static)),
256            Some(AttemptFailure::Executor(error)) => Some(error as &(dyn Error + 'static)),
257            Some(AttemptFailure::Timeout) | None => None,
258        }
259    }
260}