Skip to main content

qubit_retry/
retry_executor.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Retry executor execution.
10//!
11//! A [`RetryExecutor`] owns validated retry options, a retry decider, and
12//! optional listeners. It can execute synchronous operations or asynchronous
13//! operations on Tokio.
14
15use std::fmt;
16use std::future::Future;
17use std::time::{Duration, Instant};
18
19use qubit_common::BoxError;
20use qubit_function::{BiConsumer, BiFunction, Consumer};
21
22use crate::event::RetryListeners;
23use crate::{
24    RetryAbortContext, RetryAttemptContext, RetryAttemptFailure, RetryConfigError, RetryContext,
25    RetryDecision, RetryError, RetryFailureContext, RetryOptions, RetrySuccessContext,
26};
27
28use crate::error::RetryDecider;
29use crate::error::RetryFailureAction;
30use crate::retry_executor_builder::RetryExecutorBuilder;
31
32/// Retry executor bound to an error type.
33///
34/// The generic parameter `E` is the caller's application error type. The
35/// success type is chosen per call to [`RetryExecutor::run`],
36/// [`RetryExecutor::run_async`], or [`RetryExecutor::run_async_with_timeout`].
37/// Cloning an executor shares the retry decider and listeners through reference
38/// counting.
39#[derive(Clone)]
40pub struct RetryExecutor<E = BoxError> {
41    /// Validated limits and backoff settings (`max_attempts`, delay strategy,
42    /// jitter, optional `max_elapsed`).
43    options: RetryOptions,
44    /// Decides whether to retry after each application error; timeouts from
45    /// [`RetryAttemptFailure::AttemptTimeout`] bypass the retry decider and are treated as
46    /// retryable unless executor limits stop execution.
47    retry_decider: RetryDecider<E>,
48    /// Optional hooks invoked on success, retry scheduling, terminal failure,
49    /// or decider-initiated abort.
50    listeners: RetryListeners<E>,
51}
52
53impl<E> RetryExecutor<E> {
54    /// Creates a retry executor builder.
55    ///
56    /// # Parameters
57    /// This function has no parameters.
58    ///
59    /// # Returns
60    /// A [`RetryExecutorBuilder`] configured with default options and the default
61    /// retry-all decider.
62    ///
63    /// # Errors
64    /// This function does not return errors.
65    #[inline]
66    pub fn builder() -> RetryExecutorBuilder<E> {
67        RetryExecutorBuilder::new()
68    }
69
70    /// Creates an executor from options with the default decider.
71    ///
72    /// # Parameters
73    /// - `options`: Validated retry options to use for the executor.
74    ///
75    /// # Returns
76    /// A [`RetryExecutor`] that retries all application errors unless limits stop
77    /// execution.
78    ///
79    /// # Errors
80    /// Returns [`RetryConfigError`] if `options` fails validation.
81    pub fn from_options(options: RetryOptions) -> Result<Self, RetryConfigError> {
82        Self::builder().options(options).build()
83    }
84
85    /// Returns the immutable option snapshot used by this executor.
86    ///
87    /// # Parameters
88    /// This method has no parameters.
89    ///
90    /// # Returns
91    /// A shared reference to the executor's retry options.
92    ///
93    /// # Errors
94    /// This method does not return errors.
95    #[inline]
96    pub fn options(&self) -> &RetryOptions {
97        &self.options
98    }
99
100    /// Runs a synchronous operation with retry.
101    ///
102    /// Loops under global elapsed-time and per-executor attempt limits. Before
103    /// each try, exits with an error if the wall-clock budget is already spent
104    /// (without calling `operation` again). Success emits listener events and
105    /// returns `Ok(T)`. Failure goes through `handle_failure`: either
106    /// `std::thread::sleep` then retry, or return a terminal [`RetryError`].
107    ///
108    /// # Parameters
109    /// - `operation`: Synchronous operation to execute. It is called once per
110    ///   attempt until it returns `Ok`, the decider aborts, or retry limits
111    ///   are exhausted.
112    ///
113    /// # Returns
114    /// `Ok(T)` with the operation result, or [`RetryError`] preserving the last
115    /// application error or timeout metadata.
116    ///
117    /// # Errors
118    /// Returns [`RetryError::Aborted`] when the decider aborts,
119    /// [`RetryError::AttemptsExceeded`] when the attempt limit is reached, or
120    /// [`RetryError::MaxElapsedExceeded`] when the total elapsed-time budget is
121    /// exhausted.
122    ///
123    /// # Panics
124    /// Propagates any panic raised by `operation` or by registered listeners.
125    ///
126    /// # Blocking
127    /// This method blocks the current thread with `std::thread::sleep` between
128    /// retry attempts when the computed delay is non-zero.
129    pub fn run<T, F>(&self, mut operation: F) -> Result<T, RetryError<E>>
130    where
131        F: FnMut() -> Result<T, E>,
132    {
133        let start = Instant::now();
134        let mut attempts = 0;
135        let mut last_failure = None;
136
137        loop {
138            // Before each attempt: if total wall-clock budget is already spent,
139            // stop without calling `operation` again (consumes `last_failure`
140            // for the terminal error).
141            if let Some(error) = self.take_elapsed_error(start, attempts, &mut last_failure) {
142                return Err(error);
143            }
144
145            attempts += 1;
146            match operation() {
147                Ok(value) => {
148                    self.emit_success(attempts, start.elapsed());
149                    return Ok(value);
150                }
151                Err(error) => {
152                    let failure = RetryAttemptFailure::Error(error);
153                    // Decider + limits decide whether to sleep and retry or
154                    // finish with a terminal `RetryError`.
155                    match self.handle_failure(attempts, start, failure) {
156                        RetryFailureAction::Retry { delay, failure } => {
157                            if !delay.is_zero() {
158                                std::thread::sleep(delay);
159                            }
160                            last_failure = Some(failure);
161                        }
162                        RetryFailureAction::Finished(error) => return Err(error),
163                    }
164                }
165            }
166        }
167    }
168
169    /// Runs an asynchronous operation with retry.
170    ///
171    /// Same loop structure as [`Self::run`]: checks the global elapsed budget
172    /// before each attempt, increments the attempt counter, then runs
173    /// `operation().await`. On failure, `handle_failure` chooses a backoff
174    /// `sleep` (async) or a terminal [`RetryError`]; timing uses Tokio's timer
175    /// instead of blocking `std::thread::sleep`.
176    ///
177    /// # Parameters
178    /// - `operation`: Asynchronous operation factory. It is called once per
179    ///   attempt and must return a fresh future each time.
180    ///
181    /// # Returns
182    /// `Ok(T)` with the operation result, or [`RetryError`] preserving the last
183    /// application error.
184    ///
185    /// # Errors
186    /// Returns [`RetryError::Aborted`] when the decider aborts,
187    /// [`RetryError::AttemptsExceeded`] when the attempt limit is reached, or
188    /// [`RetryError::MaxElapsedExceeded`] when the total elapsed-time budget is
189    /// exhausted.
190    ///
191    /// # Panics
192    /// Propagates panics raised by `operation`, the returned future, or
193    /// registered listeners. Tokio may also panic if the future is polled
194    /// outside a runtime with a time driver and a non-zero sleep is required.
195    ///
196    /// # Async
197    /// Uses `tokio::time::sleep` between attempts when the computed delay is
198    /// non-zero.
199    pub async fn run_async<T, F, Fut>(&self, mut operation: F) -> Result<T, RetryError<E>>
200    where
201        F: FnMut() -> Fut,
202        Fut: Future<Output = Result<T, E>>,
203    {
204        let start = Instant::now();
205        let mut attempts = 0;
206        let mut last_failure = None;
207
208        loop {
209            // Same elapsed-budget gate as `run`: avoid scheduling another
210            // attempt once total time is exhausted.
211            if let Some(error) = self.take_elapsed_error(start, attempts, &mut last_failure) {
212                return Err(error);
213            }
214
215            attempts += 1;
216            match operation().await {
217                Ok(value) => {
218                    self.emit_success(attempts, start.elapsed());
219                    return Ok(value);
220                }
221                Err(error) => {
222                    let failure = RetryAttemptFailure::Error(error);
223                    // Decider + limits decide whether to sleep and retry or
224                    // finish with a terminal `RetryError`.
225                    match self.handle_failure(attempts, start, failure) {
226                        RetryFailureAction::Retry { delay, failure } => {
227                            sleep_async(delay).await;
228                            last_failure = Some(failure);
229                        }
230                        RetryFailureAction::Finished(error) => return Err(error),
231                    }
232                }
233            }
234        }
235    }
236
237    /// Runs an asynchronous operation with a timeout for each attempt.
238    ///
239    /// Like [`Self::run_async`], but each attempt is bounded by
240    /// `tokio::time::timeout(attempt_timeout, operation())`. Normal completion
241    /// yields the inner `Ok`/`Err`. A timeout becomes
242    /// [`RetryAttemptFailure::AttemptTimeout`] and is fed to `handle_failure` to
243    /// decide retry versus a terminal error, subject to the same global limits.
244    ///
245    /// # Parameters
246    /// - `attempt_timeout`: Maximum duration allowed for each individual
247    ///   attempt.
248    /// - `operation`: Asynchronous operation factory. It is called once per
249    ///   attempt and must return a fresh future each time.
250    ///
251    /// # Returns
252    /// `Ok(T)` with the operation result, or [`RetryError`] preserving the last
253    /// application error or timeout metadata.
254    ///
255    /// # Errors
256    /// Returns [`RetryError::Aborted`] when the decider aborts an
257    /// application error, [`RetryError::AttemptsExceeded`] when the attempt
258    /// limit is reached, or [`RetryError::MaxElapsedExceeded`] when the total
259    /// elapsed-time budget is exhausted. Attempt timeouts are represented as
260    /// [`RetryAttemptFailure::AttemptTimeout`] and are considered retryable until
261    /// limits stop execution.
262    ///
263    /// # Panics
264    /// Propagates panics raised by `operation`, the returned future, or
265    /// registered listeners. Tokio may also panic if the future is polled
266    /// outside a runtime with a time driver.
267    ///
268    /// # Async
269    /// Uses `tokio::time::timeout` for each attempt and `tokio::time::sleep`
270    /// between retries when the computed delay is non-zero.
271    pub async fn run_async_with_timeout<T, F, Fut>(
272        &self,
273        attempt_timeout: Duration,
274        mut operation: F,
275    ) -> Result<T, RetryError<E>>
276    where
277        F: FnMut() -> Fut,
278        Fut: Future<Output = Result<T, E>>,
279    {
280        let start = Instant::now();
281        let mut attempts = 0;
282        let mut last_failure = None;
283
284        loop {
285            // Total elapsed budget check before starting the timed attempt.
286            if let Some(error) = self.take_elapsed_error(start, attempts, &mut last_failure) {
287                return Err(error);
288            }
289
290            attempts += 1;
291            let attempt_start = Instant::now();
292            // Outer `Result` is from `tokio::time::timeout`; inner is the
293            // operation's `Result<T, E>`.
294            match tokio::time::timeout(attempt_timeout, operation()).await {
295                Ok(Ok(value)) => {
296                    self.emit_success(attempts, start.elapsed());
297                    return Ok(value);
298                }
299                Ok(Err(error)) => {
300                    let failure = RetryAttemptFailure::Error(error);
301                    match self.handle_failure(attempts, start, failure) {
302                        RetryFailureAction::Retry { delay, failure } => {
303                            sleep_async(delay).await;
304                            last_failure = Some(failure);
305                        }
306                        RetryFailureAction::Finished(error) => return Err(error),
307                    }
308                }
309                Err(_) => {
310                    // Per-attempt budget exceeded before the future completed.
311                    let failure = RetryAttemptFailure::AttemptTimeout {
312                        elapsed: attempt_start.elapsed(),
313                        timeout: attempt_timeout,
314                    };
315                    match self.handle_failure(attempts, start, failure) {
316                        RetryFailureAction::Retry { delay, failure } => {
317                            sleep_async(delay).await;
318                            last_failure = Some(failure);
319                        }
320                        RetryFailureAction::Finished(error) => return Err(error),
321                    }
322                }
323            }
324        }
325    }
326
327    /// Creates an executor from already validated parts.
328    ///
329    /// # Parameters
330    /// - `options`: Retry options used by the executor.
331    /// - `retry_decider`: [`RetryDecider`] shared by cloned executors (uses each failure's `E`).
332    /// - `listeners`: Optional callbacks invoked during execution.
333    ///
334    /// # Returns
335    /// A new [`RetryExecutor`].
336    ///
337    /// # Errors
338    /// This function does not return errors. Callers must validate `options`
339    /// before constructing the executor.
340    pub(super) fn new(
341        options: RetryOptions,
342        retry_decider: RetryDecider<E>,
343        listeners: RetryListeners<E>,
344    ) -> Self {
345        Self {
346            options,
347            retry_decider,
348            listeners,
349        }
350    }
351
352    /// Handles a failed attempt and decides the next executor action.
353    ///
354    /// # Processing
355    /// 1. Builds [`RetryAttemptContext`] from elapsed time and attempt counts, then
356    ///    asks the [`RetryDecider`]: application errors `E` go through it;
357    ///    [`RetryAttemptFailure::AttemptTimeout`] is treated as retryable unless
358    ///    limits below apply.
359    /// 2. If the decision is [`RetryDecision::Abort`], emits the abort listener
360    ///    and returns [`RetryFailureAction::Finished`] with [`RetryError::Aborted`].
361    /// 3. If `attempts` has reached `max_attempts`, emits the failure listener
362    ///    and returns [`RetryFailureAction::Finished`] with
363    ///    [`RetryError::AttemptsExceeded`].
364    /// 4. Otherwise computes base delay for this attempt, applies jitter, and if
365    ///    `max_elapsed` is set and sleeping would exceed the total time budget,
366    ///    emits the failure listener and returns [`RetryFailureAction::Finished`]
367    ///    with [`RetryError::MaxElapsedExceeded`].
368    /// 5. Otherwise emits the retry listener and returns [`RetryFailureAction::Retry`]
369    ///    with the computed delay and the same failure payload.
370    ///
371    /// # Parameters
372    /// - `attempts`: Number of attempts executed so far.
373    /// - `start`: Start time of the whole retry execution.
374    /// - `failure`: Failure produced by the latest attempt.
375    ///
376    /// # Returns
377    /// [`RetryFailureAction::Retry`] with the computed delay when retrying should
378    /// continue, or [`RetryFailureAction::Finished`] with the terminal
379    /// [`RetryError`].
380    ///
381    /// # Errors
382    /// This function does not return errors directly; terminal retry errors are
383    /// returned inside [`RetryFailureAction::Finished`].
384    ///
385    /// # Side Effects
386    /// Invokes retry, failure, or abort listeners depending on the decision.
387    fn handle_failure(
388        &self,
389        attempts: u32,
390        start: Instant,
391        failure: RetryAttemptFailure<E>,
392    ) -> RetryFailureAction<E> {
393        let elapsed = start.elapsed();
394        let context = RetryAttemptContext {
395            attempt: attempts,
396            max_attempts: self.options.max_attempts.get(),
397            elapsed,
398        };
399        // Application errors consult the decider; attempt timeouts are
400        // always retryable unless limits below say otherwise.
401        let decision = match &failure {
402            RetryAttemptFailure::Error(error) => self.retry_decider.apply(error, &context),
403            RetryAttemptFailure::AttemptTimeout { .. } => RetryDecision::Retry,
404        };
405        if decision == RetryDecision::Abort {
406            self.emit_abort(attempts, elapsed, &failure);
407            return RetryFailureAction::Finished(RetryError::Aborted {
408                attempts,
409                elapsed,
410                failure,
411            });
412        }
413
414        // `attempts` is the count including this failure; reaching
415        // `max_attempts` means no further attempts remain.
416        let max_attempts = self.options.max_attempts.get();
417        if attempts >= max_attempts {
418            let Some(failure) = self.emit_failure(attempts, elapsed, Some(failure)) else {
419                unreachable!("failure must exist when attempts exceed max_attempts");
420            };
421            return RetryFailureAction::Finished(RetryError::AttemptsExceeded {
422                attempts,
423                max_attempts,
424                elapsed,
425                last_failure: failure,
426            });
427        }
428
429        let base_delay = self.options.delay.base_delay(attempts);
430        let delay = self.options.jitter.apply(base_delay);
431        // If sleeping would push total elapsed past the budget, finish now
432        // instead of sleeping once and failing on the next loop iteration.
433        if let Some(max_elapsed) = self.options.max_elapsed {
434            if will_exceed_elapsed(start.elapsed(), delay, max_elapsed) {
435                let last_failure = self.emit_failure(attempts, elapsed, Some(failure));
436                let error = RetryError::MaxElapsedExceeded {
437                    attempts,
438                    elapsed,
439                    max_elapsed,
440                    last_failure,
441                };
442                return RetryFailureAction::Finished(error);
443            }
444        }
445
446        self.emit_retry(attempts, elapsed, delay, &failure);
447        RetryFailureAction::Retry { delay, failure }
448    }
449
450    /// Checks whether the total elapsed-time budget has already been reached.
451    ///
452    /// # Parameters
453    /// - `start`: Start time of the whole retry execution.
454    /// - `attempts`: Number of attempts executed so far.
455    /// - `last_failure`: Mutable slot containing the last failure, consumed
456    ///   when the elapsed budget is exceeded.
457    ///
458    /// # Returns
459    /// `Some(RetryError)` when the elapsed budget is exhausted; otherwise
460    /// `None`.
461    ///
462    /// # Errors
463    /// This function does not return errors directly; the terminal error is
464    /// returned as `Some`.
465    ///
466    /// # Side Effects
467    /// Emits a failure event when the elapsed budget is exhausted.
468    fn take_elapsed_error(
469        &self,
470        start: Instant,
471        attempts: u32,
472        last_failure: &mut Option<RetryAttemptFailure<E>>,
473    ) -> Option<RetryError<E>> {
474        let max_elapsed = self.options.max_elapsed?;
475        let elapsed = start.elapsed();
476        // Still within budget: allow another attempt (and a possible inter-attempt
477        // sleep).
478        if elapsed < max_elapsed {
479            return None;
480        }
481        // Budget already consumed: do not start another attempt; attach the
482        // last failure if we have one from the previous attempt.
483        let last_failure = self.emit_failure(attempts, elapsed, last_failure.take());
484        Some(RetryError::MaxElapsedExceeded {
485            attempts,
486            elapsed,
487            max_elapsed,
488            last_failure,
489        })
490    }
491
492    /// Emits the retry listener event when a listener is registered.
493    ///
494    /// # Parameters
495    /// - `attempt`: Attempt that just failed.
496    /// - `elapsed`: Total elapsed duration before sleeping.
497    /// - `next_delay`: RetryDelay that will be slept before the next attempt.
498    /// - `failure`: Failure that triggered the retry.
499    ///
500    /// # Returns
501    /// This function returns nothing.
502    ///
503    /// # Errors
504    /// This function does not return errors.
505    ///
506    /// # Panics
507    /// Propagates any panic raised by the listener.
508    fn emit_retry(
509        &self,
510        attempt: u32,
511        elapsed: Duration,
512        next_delay: Duration,
513        failure: &RetryAttemptFailure<E>,
514    ) {
515        if let Some(listener) = &self.listeners.retry {
516            listener.accept(
517                &RetryContext {
518                    attempt,
519                    max_attempts: self.options.max_attempts.get(),
520                    elapsed,
521                    next_delay,
522                },
523                failure,
524            );
525        }
526    }
527
528    /// Emits the success listener event when a listener is registered.
529    ///
530    /// # Parameters
531    /// - `attempts`: Number of attempts that were executed.
532    /// - `elapsed`: Total elapsed duration at success.
533    ///
534    /// # Returns
535    /// This function returns nothing.
536    ///
537    /// # Errors
538    /// This function does not return errors.
539    ///
540    /// # Panics
541    /// Propagates any panic raised by the listener.
542    fn emit_success(&self, attempts: u32, elapsed: Duration) {
543        if let Some(listener) = &self.listeners.success {
544            listener.accept(&RetrySuccessContext { attempts, elapsed });
545        }
546    }
547
548    /// Emits the failure listener event when a listener is registered.
549    ///
550    /// # Parameters
551    /// - `attempts`: Number of attempts that were executed.
552    /// - `elapsed`: Total elapsed duration at failure.
553    /// - `failure`: Final failure, or `None` if no attempt ran.
554    ///
555    /// # Returns
556    /// This function returns nothing.
557    ///
558    /// # Errors
559    /// This function does not return errors.
560    ///
561    /// # Panics
562    /// Propagates any panic raised by the listener.
563    fn emit_failure(
564        &self,
565        attempts: u32,
566        elapsed: Duration,
567        failure: Option<RetryAttemptFailure<E>>,
568    ) -> Option<RetryAttemptFailure<E>> {
569        if let Some(listener) = &self.listeners.failure {
570            listener.accept(&RetryFailureContext { attempts, elapsed }, &failure);
571        }
572        failure
573    }
574
575    /// Emits the abort listener event when a listener is registered.
576    ///
577    /// # Parameters
578    /// - `attempts`: Number of attempts that were executed.
579    /// - `elapsed`: Total elapsed duration at abort.
580    /// - `failure`: Failure that caused the decider to abort retrying.
581    ///
582    /// # Returns
583    /// This function returns nothing.
584    ///
585    /// # Errors
586    /// This function does not return errors.
587    ///
588    /// # Panics
589    /// Propagates any panic raised by the listener.
590    fn emit_abort(&self, attempts: u32, elapsed: Duration, failure: &RetryAttemptFailure<E>) {
591        if let Some(listener) = &self.listeners.abort {
592            listener.accept(&RetryAbortContext { attempts, elapsed }, failure);
593        }
594    }
595}
596
597impl<E> fmt::Debug for RetryExecutor<E> {
598    /// Formats the executor for debug output without exposing callbacks.
599    ///
600    /// # Parameters
601    /// - `f`: Formatter provided by the standard formatting machinery.
602    ///
603    /// # Returns
604    /// `fmt::Result` from the formatter.
605    ///
606    /// # Errors
607    /// Returns a formatting error if the underlying formatter fails.
608    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609        f.debug_struct("RetryExecutor")
610            .field("options", &self.options)
611            .finish_non_exhaustive()
612    }
613}
614
615/// Checks whether sleeping would exhaust the elapsed-time budget.
616///
617/// # Parameters
618/// - `elapsed`: Duration already consumed by the retry execution.
619/// - `delay`: RetryDelay that would be slept before the next attempt.
620/// - `max_elapsed`: Configured total elapsed-time budget.
621///
622/// # Returns
623/// `true` when `elapsed + delay` is greater than or equal to `max_elapsed`, or
624/// when duration addition overflows.
625///
626/// # Errors
627/// This function does not return errors.
628fn will_exceed_elapsed(elapsed: Duration, delay: Duration, max_elapsed: Duration) -> bool {
629    // Treat overflow as "would exceed" so we never sleep with a bogus huge duration.
630    elapsed
631        .checked_add(delay)
632        .map_or(true, |next_elapsed| next_elapsed >= max_elapsed)
633}
634
635/// Sleeps asynchronously when the delay is non-zero.
636///
637/// # Parameters
638/// - `delay`: Duration to sleep.
639///
640/// # Returns
641/// This function returns after the sleep completes, or immediately for a zero
642/// delay.
643///
644/// # Errors
645/// This function does not return errors.
646///
647/// # Panics
648/// Tokio may panic if this future is polled outside a runtime with a time
649/// driver and `delay` is non-zero.
650async fn sleep_async(delay: Duration) {
651    if !delay.is_zero() {
652        tokio::time::sleep(delay).await;
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    /// Verifies elapsed-budget checks handle below-bound, at-bound, and
661    /// overflowing durations.
662    ///
663    /// # Parameters
664    /// This test has no parameters.
665    ///
666    /// # Returns
667    /// This test returns nothing.
668    ///
669    /// # Errors
670    /// The test fails through assertions when elapsed-budget checks return the
671    /// wrong decision.
672    #[test]
673    fn test_will_exceed_elapsed_handles_boundaries_and_overflow() {
674        let one_ms = Duration::from_millis(1);
675        let two_ms = Duration::from_millis(2);
676
677        assert!(!will_exceed_elapsed(one_ms, Duration::ZERO, two_ms));
678        assert!(will_exceed_elapsed(one_ms, one_ms, two_ms));
679        assert!(will_exceed_elapsed(Duration::MAX, one_ms, Duration::MAX));
680    }
681
682    /// Verifies async sleep returns for zero and non-zero delays.
683    ///
684    /// # Parameters
685    /// This test has no parameters.
686    ///
687    /// # Returns
688    /// This test returns nothing.
689    ///
690    /// # Errors
691    /// The test fails if Tokio's timer does not complete either sleep.
692    #[tokio::test]
693    async fn test_sleep_async_handles_zero_and_nonzero_delays() {
694        sleep_async(Duration::ZERO).await;
695        sleep_async(Duration::from_millis(1)).await;
696    }
697
698    /// Verifies the default boxed-error executor type runs success and failure
699    /// paths.
700    ///
701    /// # Parameters
702    /// This test has no parameters.
703    ///
704    /// # Returns
705    /// This test returns nothing.
706    ///
707    /// # Errors
708    /// The test fails through assertions when default boxed-error execution
709    /// returns incorrect metadata.
710    #[test]
711    fn test_box_error_executor_runs_success_and_failure_paths() {
712        let executor: RetryExecutor<BoxError> = RetryExecutor::builder()
713            .max_attempts(1)
714            .delay(crate::RetryDelay::none())
715            .build()
716            .expect("executor should be built");
717
718        assert_eq!(executor.options().max_attempts.get(), 1);
719        assert!(format!("{executor:?}").contains("RetryExecutor"));
720        let value = executor
721            .run(|| Ok::<_, BoxError>("default-box-error"))
722            .expect("boxed-error executor should return success");
723        assert_eq!(value, "default-box-error");
724
725        let error = executor
726            .run(|| -> Result<(), BoxError> {
727                let source = std::io::Error::new(std::io::ErrorKind::Other, "boxed failure");
728                Err(Box::new(source) as BoxError)
729            })
730            .expect_err("single failed attempt should exceed attempts");
731
732        assert_eq!(error.attempts(), 1);
733        assert_eq!(
734            error.last_error().map(ToString::to_string).as_deref(),
735            Some("boxed failure")
736        );
737    }
738}