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}