try_again/
lib.rs

1//! # try-again
2//!
3//! Retry **synchronous** or **asynchronous** operations until they no longer can or need to be retried.
4//!
5//! Provides `fn` `retry` for retrying synchronous operations.
6//!
7//! Provides `async fn` `retry_async` for retrying asynchronous operations.
8//!
9//! Supports closures and function pointers.
10//!
11//! The retried operation may return any type that implements `NeedsRetry`.
12//! This trait is already implemented for `Result`, `Option` and `ExitStatus`, allowing you to retry common fallible
13//! outcomes.
14//!
15//! ## Synchronous example
16//!
17//! ```rust
18//! use assertr::prelude::*;
19//! use try_again::{delay, retry, IntoStdDuration};
20//!
21//! async fn do_smth() {
22//!     fn fallible_operation() -> Result<(), ()> {
23//!         Ok(())
24//!     }
25//!
26//!     let final_outcome = retry(fallible_operation)
27//!         .delayed_by(delay::Fixed::of(125.millis()).take(5));
28//!
29//!     assert_that(final_outcome).is_ok();
30//! }
31//! ```
32//!
33//! ## Asynchronous example
34//!
35//! ```rust
36//! use assertr::prelude::*;
37//! use try_again::{delay, retry_async, IntoStdDuration};
38//!
39//! async fn do_smth() {
40//!     async fn fallible_operation() -> Result<(), ()> {
41//!         Ok(())
42//!     }
43//!
44//!     let final_outcome = retry_async(fallible_operation)
45//!         .delayed_by(delay::ExponentialBackoff::of_initial_delay(125.millis()).capped_at(2.secs()).take(10))
46//!         .await;
47//!
48//!     assert_that(final_outcome).is_ok();
49//! }
50//! ```
51//!
52//! ## Details
53//!
54//! ### Delay strategies
55//!
56//! `delayed_by` accepts a delay strategy. The `delay` module provides the following implementations
57//!
58//! - `None`: No delay is applied.
59//! - `Fixed`: A static delay.
60//! - `ExponentialBackoff`: An exponentially increasing delay
61//!
62//! All work with `std::time::Duration`, re-exposed as `StdDuration`. The `IntoStdDuration` can be used for a fluent syntax
63//! when defining durations, like in
64//!
65//! use try_again::{delay, IntoStdDuration};
66//!
67//! delay::Fixed::of(250.millis())
68//!
69//! ### Delay executors
70//!
71//! The standard `retry` and `retry_async` functions have the following default behavior:
72//!
73//! - `retry` puts the current thread to sleep between retries (through the provided `ThreadSleep` executor).
74//! - `retry_async` instructs the tokio runtime to sleep between retries (through the provided `TokioSleep` executor,
75//!   requires the `async-tokio` feature (enabled by default)).
76//!
77//! The `retry_with_options` and `retry_async_with_options` functions can be used to overwrite the standard behavior
78//! with any executor type implementing the `DelayExecutor` trait.
79//!
80//! That way, support for `async_std` or other asynchronous runtimes could be provided.
81
82#![forbid(unsafe_code)]
83#![deny(clippy::unwrap_used)]
84
85pub mod delay;
86pub mod delay_executor;
87pub mod delay_strategy;
88mod duration;
89mod fallible;
90mod tracked_iterator;
91
92use std::fmt::Debug;
93use std::marker::PhantomData;
94
95#[cfg(feature = "async")]
96use crate::delay_executor::AsyncDelayExecutor;
97use crate::delay_executor::DelayExecutor;
98use crate::delay_executor::ThreadSleep;
99#[cfg(feature = "async-tokio")]
100use crate::delay_executor::TokioSleep;
101use crate::delay_strategy::DelayStrategy;
102
103pub use duration::IntoStdDuration;
104pub use duration::StdDuration;
105pub use fallible::NeedsRetry;
106
107#[tracing::instrument(level = "debug", name = "retry", skip(operation))]
108#[must_use = "Call `delayed_by` on the returned value to complete the retry strategy configuration."]
109pub fn retry<Out, Op>(operation: Op) -> NeedsDelayStrategy<Out, Op>
110where
111    Out: NeedsRetry + Debug,
112    Op: Fn() -> Out,
113{
114    NeedsDelayStrategy { operation }
115}
116
117pub struct NeedsDelayStrategy<Out, Op>
118where
119    Out: NeedsRetry + Debug,
120    Op: Fn() -> Out,
121{
122    operation: Op,
123}
124
125impl<Out, Op> NeedsDelayStrategy<Out, Op>
126where
127    Out: NeedsRetry + Debug,
128    Op: Fn() -> Out,
129{
130    pub fn delayed_by<DelayStrat>(self, delay: DelayStrat) -> Out
131    where
132        DelayStrat: DelayStrategy<StdDuration>,
133    {
134        retry_with_options(
135            self.operation,
136            RetryOptions {
137                delay_strategy: delay,
138                delay_executor: ThreadSleep,
139                _marker: PhantomData,
140            },
141        )
142    }
143}
144
145#[derive(Debug)]
146pub struct RetryOptions<
147    Delay: Debug + Clone,
148    DelayStrat: DelayStrategy<Delay>,
149    DelayExec: DelayExecutor<Delay>,
150> {
151    pub delay_strategy: DelayStrat,
152    pub delay_executor: DelayExec,
153    pub _marker: PhantomData<Delay>,
154}
155
156#[tracing::instrument(level = "debug", name = "retry_with_options", skip(operation))]
157pub fn retry_with_options<Delay, DelayStrat, DelayExec, Out, Op>(
158    operation: Op,
159    mut options: RetryOptions<Delay, DelayStrat, DelayExec>,
160) -> Out
161where
162    Delay: Debug + Clone,
163    DelayStrat: DelayStrategy<Delay> + Debug,
164    DelayExec: DelayExecutor<Delay> + Debug,
165    Out: NeedsRetry + Debug,
166    Op: Fn() -> Out,
167{
168    let mut tries: usize = 1;
169    loop {
170        let out = operation();
171        match out.needs_retry() {
172            false => return out,
173            true => match options.delay_strategy.next_delay() {
174                Some(delay) => {
175                    tracing::debug!(tries, delay = ?delay, "Operation was not successful. Waiting...");
176                    options.delay_executor.delay_by(delay.clone());
177                    tries += 1;
178                }
179                None => {
180                    tracing::error!(tries, last_output = ?out, "Operation was not successful after maximum retries. Aborting with last output seen.");
181                    return out;
182                }
183            },
184        };
185    }
186}
187
188#[cfg(feature = "async")]
189#[tracing::instrument(level = "debug", name = "retry_async", skip(operation))]
190pub fn retry_async<Out, Op>(operation: Op) -> AsyncNeedsDelayStrategy<Out, Op>
191where
192    Out: NeedsRetry + Debug,
193    Op: AsyncFn() -> Out,
194{
195    AsyncNeedsDelayStrategy { operation }
196}
197
198#[cfg(feature = "async")]
199pub struct AsyncNeedsDelayStrategy<Out, Op>
200where
201    Out: NeedsRetry + Debug,
202    Op: AsyncFn() -> Out,
203{
204    operation: Op,
205}
206
207#[cfg(feature = "async")]
208impl<Out, Op> AsyncNeedsDelayStrategy<Out, Op>
209where
210    Out: NeedsRetry + Debug,
211    Op: AsyncFn() -> Out,
212{
213    pub async fn delayed_by<DelayStrat>(self, delay: DelayStrat) -> Out
214    where
215        DelayStrat: DelayStrategy<StdDuration>,
216    {
217        retry_async_with_options(
218            self.operation,
219            RetryAsyncOptions {
220                delay_strategy: delay,
221                delay_executor: TokioSleep,
222                _marker: PhantomData,
223            },
224        )
225        .await
226    }
227}
228
229#[cfg(feature = "async")]
230#[derive(Debug)]
231pub struct RetryAsyncOptions<
232    Delay: Debug + Clone,
233    DelayStrat: DelayStrategy<Delay>,
234    DelayExec: AsyncDelayExecutor<Delay>,
235> {
236    pub delay_strategy: DelayStrat,
237    pub delay_executor: DelayExec,
238    pub _marker: PhantomData<Delay>,
239}
240
241#[cfg(feature = "async")]
242#[tracing::instrument(
243    level = "debug",
244    name = "retry_async_with_delay_strategy",
245    skip(operation)
246)]
247pub async fn retry_async_with_options<Delay, DelayStrat, DelayExec, Out>(
248    operation: impl AsyncFn() -> Out,
249    mut options: RetryAsyncOptions<Delay, DelayStrat, DelayExec>,
250) -> Out
251where
252    Delay: Debug + Clone,
253    DelayStrat: DelayStrategy<Delay>,
254    DelayExec: AsyncDelayExecutor<Delay>,
255    Out: NeedsRetry + Debug,
256{
257    let mut tries: usize = 1;
258    loop {
259        let out = operation().await;
260        match out.needs_retry() {
261            false => return out,
262            true => match options.delay_strategy.next_delay() {
263                Some(delay) => {
264                    tracing::debug!(tries, delay = ?delay, "Operation was not successful. Waiting...");
265                    options.delay_executor.delay_by(delay.clone()).await;
266                    tries += 1;
267                }
268                None => {
269                    tracing::error!(tries, last_output = ?out, "Operation was not successful after maximum retries. Aborting with last output seen.");
270                    return out;
271                }
272            },
273        };
274    }
275}