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