tryhard/
lib.rs

1//! Easily retry futures.
2//!
3//! ## Example usage
4//!
5//! ```
6//! // some async function that can fail
7//! async fn read_file(path: &str) -> Result<String, std::io::Error> {
8//!     // ...
9//!     # Ok("tryhard".to_string())
10//! }
11//!
12//! # futures::executor::block_on(async_try_main()).unwrap();
13//! #
14//! # async fn async_try_main() -> Result<(), Box<dyn std::error::Error>> {
15//! let contents = tryhard::retry_fn(|| read_file("Cargo.toml"))
16//!     // retry at most 10 times
17//!     .retries(10)
18//!     .await?;
19//!
20//! assert!(contents.contains("tryhard"));
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! You can also customize which backoff strategy to use and what the max retry delay should be:
26//!
27//! ```
28//! use std::time::Duration;
29//!
30//! # async fn read_file(path: &str) -> Result<String, std::io::Error> {
31//! #     Ok("tryhard".to_string())
32//! # }
33//! # futures::executor::block_on(async_try_main()).unwrap();
34//! #
35//! # async fn async_try_main() -> Result<(), Box<dyn std::error::Error>> {
36//! let contents = tryhard::retry_fn(|| read_file("Cargo.toml"))
37//!     .retries(10)
38//!     .exponential_backoff(Duration::from_millis(10))
39//!     .max_delay(Duration::from_secs(1))
40//!     .await?;
41//!
42//! assert!(contents.contains("tryhard"));
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ## Retrying several futures in the same way
48//!
49//! Using [`RetryFutureConfig`] you're able to retry several futures in the same way:
50//!
51//! ```
52//! # use std::time::Duration;
53//! # async fn read_file(path: &str) -> Result<String, std::io::Error> {
54//! #     Ok("tryhard".to_string())
55//! # }
56//! #
57//! # futures::executor::block_on(async_try_main()).unwrap();
58//! #
59//! # async fn async_try_main() -> Result<(), Box<dyn std::error::Error>> {
60//! use tryhard::RetryFutureConfig;
61//!
62//! let config = RetryFutureConfig::new(10)
63//!     .exponential_backoff(Duration::from_millis(10))
64//!     .max_delay(Duration::from_secs(3));
65//!
66//! tryhard::retry_fn(|| read_file("Cargo.toml"))
67//!     .with_config(config)
68//!     .await?;
69//!
70//! // retry another future in the same way
71//! tryhard::retry_fn(|| read_file("src/lib.rs"))
72//!     .with_config(config)
73//!     .await?;
74//! # Ok(())
75//! # }
76//! ```
77//!
78//! ## How many times will my future run?
79//!
80//! The future is always run at least once, so if you do `.retries(0)` your future will run once.
81//! If you do `.retries(10)` and your future always fails it'll run 11 times.
82//!
83//! ## Why do you require a closure?
84//!
85//! Due to how futures work in Rust you're not able to retry a bare `F where F: Future`. A future
86//! can possibly fail at any point in its execution and might be in an inconsistent state after the
87//! failing. Therefore retrying requires making a fresh future for each attempt.
88//!
89//! This means you cannot move values into the closure that produces the futures. You'll have to
90//! clone instead:
91//!
92//! ```
93//! async fn future_with_owned_data(data: Vec<u8>) -> Result<(), std::io::Error> {
94//!     // ...
95//!     # Ok(())
96//! }
97//!
98//! # futures::executor::block_on(async_try_main()).unwrap();
99//! #
100//! # async fn async_try_main() -> Result<(), Box<dyn std::error::Error>> {
101//! let data: Vec<u8> = vec![1, 2, 3];
102//!
103//! tryhard::retry_fn(|| {
104//!     // We need to clone `data` here. Otherwise we would have to move `data` into the closure.
105//!     // `move` closures can only be called once (they only implement `FnOnce`)
106//!     // and therefore cannot be used to create more than one future.
107//!     let data = data.clone();
108//!
109//!     async {
110//!         future_with_owned_data(data).await
111//!     }
112//! }).retries(10).await?;
113//! # Ok(())
114//! # }
115//! ```
116//!
117//! ## Be careful what you retry
118//!
119//! This library is meant to make it straight forward to retry simple futures, such as sending a
120//! single request to some service that occationally fails. If you have some complex operation that
121//! consists of multiple futures each of which can fail, this library might be not appropriate. You
122//! risk repeating the same operation more than once because some later operation keeps failing.
123//!
124//! ## Tokio only for now
125//!
126//! This library currently expects to be used from within a [tokio](https://tokio.rs) runtime. That
127//! is because it makes use of async timers. Feel free to open an issue if you need support for
128//! other runtimes.
129//!
130//! [`RetryFuture`]: struct.RetryFuture.html
131
132// BEGIN - Embark standard lints v0.3
133// do not change or add/remove here, but one can add exceptions after this section
134// for more info see: <https://github.com/EmbarkStudios/rust-ecosystem/issues/59>
135#![deny(unsafe_code)]
136#![warn(
137    clippy::all,
138    clippy::await_holding_lock,
139    clippy::dbg_macro,
140    clippy::debug_assert_with_mut_call,
141    clippy::doc_markdown,
142    clippy::empty_enum,
143    clippy::enum_glob_use,
144    clippy::exit,
145    clippy::explicit_into_iter_loop,
146    clippy::filter_map_next,
147    clippy::fn_params_excessive_bools,
148    clippy::if_let_mutex,
149    clippy::imprecise_flops,
150    clippy::inefficient_to_string,
151    clippy::large_types_passed_by_value,
152    clippy::let_unit_value,
153    clippy::linkedlist,
154    clippy::lossy_float_literal,
155    clippy::macro_use_imports,
156    clippy::map_err_ignore,
157    clippy::map_flatten,
158    clippy::map_unwrap_or,
159    clippy::match_on_vec_items,
160    clippy::match_same_arms,
161    clippy::match_wildcard_for_single_variants,
162    clippy::mem_forget,
163    clippy::needless_borrow,
164    clippy::needless_continue,
165    clippy::option_option,
166    clippy::ref_option_ref,
167    clippy::rest_pat_in_fully_bound_structs,
168    clippy::string_add_assign,
169    clippy::string_add,
170    clippy::string_to_string,
171    clippy::suboptimal_flops,
172    clippy::todo,
173    clippy::unimplemented,
174    clippy::unnested_or_patterns,
175    clippy::unused_self,
176    clippy::verbose_file_reads,
177    unexpected_cfgs,
178    future_incompatible,
179    nonstandard_style,
180    rust_2018_idioms
181)]
182// END - Embark standard lints v0.3
183#![warn(missing_docs)]
184#![deny(rustdoc::broken_intra_doc_links)]
185
186use backoff_strategies::{
187    BackoffStrategy, ExponentialBackoff, FixedBackoff, LinearBackoff, NoBackoff,
188};
189use pin_project_lite::pin_project;
190use std::time::Duration;
191use std::{
192    fmt,
193    future::Future,
194    pin::Pin,
195    task::{Context, Poll},
196};
197
198mod on_retry;
199
200pub mod backoff_strategies;
201
202pub use on_retry::{NoOnRetry, OnRetry};
203
204/// Create a `RetryFn` which produces retryable futures.
205pub fn retry_fn<F>(f: F) -> RetryFn<F> {
206    RetryFn { f }
207}
208
209/// A type that produces retryable futures.
210#[derive(Debug)]
211pub struct RetryFn<F> {
212    f: F,
213}
214
215impl<F, Fut, T, E> RetryFn<F>
216where
217    F: FnMut() -> Fut,
218    Fut: Future<Output = Result<T, E>>,
219{
220    /// Specify the number of times to retry the future.
221    pub fn retries(self, max_retries: u32) -> RetryFuture<F, Fut, NoBackoff, NoOnRetry> {
222        self.with_config(RetryFutureConfig::new(max_retries))
223    }
224
225    /// Create a retryable future from the given configuration.
226    pub fn with_config<BackoffT, OnRetryT>(
227        self,
228        config: RetryFutureConfig<BackoffT, OnRetryT>,
229    ) -> RetryFuture<F, Fut, BackoffT, OnRetryT> {
230        RetryFuture {
231            make_future: self.f,
232            attempts_remaining: config.max_retries,
233            state: RetryState::NotStarted,
234            attempt: 0,
235            config,
236        }
237    }
238}
239
240pin_project! {
241    /// A retryable future.
242    ///
243    /// Can be created by calling [`retry_fn`](fn.retry_fn.html).
244    pub struct RetryFuture<MakeFutureT, FutureT, BackoffT, OnRetryT> {
245        make_future: MakeFutureT,
246        attempts_remaining: u32,
247        #[pin]
248        state: RetryState<FutureT>,
249        attempt: u32,
250        config: RetryFutureConfig<BackoffT, OnRetryT>,
251    }
252}
253
254impl<MakeFutureT, FutureT, BackoffT, T, E, OnRetryT>
255    RetryFuture<MakeFutureT, FutureT, BackoffT, OnRetryT>
256where
257    MakeFutureT: FnMut() -> FutureT,
258    FutureT: Future<Output = Result<T, E>>,
259{
260    /// Set the max duration to sleep between each attempt.
261    #[inline]
262    pub fn max_delay(mut self, delay: Duration) -> Self {
263        self.config = self.config.max_delay(delay);
264        self
265    }
266
267    /// Remove the backoff strategy.
268    ///
269    /// This will make the future be retried immediately without any delay in between attempts.
270    #[inline]
271    pub fn no_backoff(self) -> RetryFuture<MakeFutureT, FutureT, NoBackoff, OnRetryT> {
272        self.custom_backoff(NoBackoff)
273    }
274
275    /// Use exponential backoff for retrying the future.
276    ///
277    /// The first delay will be `initial_delay` and afterwards the delay will double every time.
278    #[inline]
279    pub fn exponential_backoff(
280        self,
281        initial_delay: Duration,
282    ) -> RetryFuture<MakeFutureT, FutureT, ExponentialBackoff, OnRetryT> {
283        self.custom_backoff(ExponentialBackoff {
284            delay: initial_delay,
285        })
286    }
287
288    /// Use a fixed backoff for retrying the future.
289    ///
290    /// The delay between attempts will always be `delay`.
291    #[inline]
292    pub fn fixed_backoff(
293        self,
294        delay: Duration,
295    ) -> RetryFuture<MakeFutureT, FutureT, FixedBackoff, OnRetryT> {
296        self.custom_backoff(FixedBackoff { delay })
297    }
298
299    /// Use a linear backoff for retrying the future.
300    ///
301    /// The delay will be `delay * attempt` so it'll scale linear with the attempt.
302    #[inline]
303    pub fn linear_backoff(
304        self,
305        delay: Duration,
306    ) -> RetryFuture<MakeFutureT, FutureT, LinearBackoff, OnRetryT> {
307        self.custom_backoff(LinearBackoff { delay })
308    }
309
310    /// Use a custom backoff specified by some function.
311    ///
312    /// ```
313    /// use std::time::Duration;
314    ///
315    /// # async fn read_file(path: &str) -> Result<String, std::io::Error> {
316    /// #     todo!()
317    /// # }
318    /// #
319    /// # async fn async_try_main() -> Result<(), Box<dyn std::error::Error>> {
320    /// tryhard::retry_fn(|| read_file("Cargo.toml"))
321    ///     .retries(10)
322    ///     .custom_backoff(|attempt, _error: &std::io::Error| {
323    ///         if attempt < 5 {
324    ///             Duration::from_millis(100)
325    ///         } else {
326    ///             Duration::from_millis(500)
327    ///         }
328    ///     })
329    ///     .await?;
330    /// # Ok(())
331    /// # }
332    /// ```
333    ///
334    /// You can also stop retrying early:
335    ///
336    /// ```
337    /// use std::time::Duration;
338    /// use tryhard::RetryPolicy;
339    ///
340    /// # async fn read_file(path: &str) -> Result<String, std::io::Error> {
341    /// #     todo!()
342    /// # }
343    /// #
344    /// # async fn async_try_main() -> Result<(), Box<dyn std::error::Error>> {
345    /// tryhard::retry_fn(|| read_file("Cargo.toml"))
346    ///     .retries(10)
347    ///     .custom_backoff(|attempt, error: &std::io::Error| {
348    ///         if error.to_string().contains("foobar") {
349    ///             // returning this will cancel the loop and
350    ///             // return the most recent error
351    ///             RetryPolicy::Break
352    ///         } else {
353    ///             RetryPolicy::Delay(Duration::from_millis(50))
354    ///         }
355    ///     })
356    ///     .await?;
357    /// # Ok(())
358    /// # }
359    /// ```
360    #[inline]
361    pub fn custom_backoff<B>(
362        self,
363        backoff_strategy: B,
364    ) -> RetryFuture<MakeFutureT, FutureT, B, OnRetryT>
365    where
366        for<'a> B: BackoffStrategy<'a, E>,
367    {
368        RetryFuture {
369            make_future: self.make_future,
370            attempts_remaining: self.attempts_remaining,
371            state: self.state,
372            attempt: self.attempt,
373            config: self.config.custom_backoff(backoff_strategy),
374        }
375    }
376
377    /// Some async computation that will be spawned before each retry.
378    ///
379    /// This can for example be used for telemtry such as logging or other kinds of tracking.
380    ///
381    /// The future returned will be given to `tokio::spawn` so wont impact the actual retrying.
382    ///
383    /// ## Example
384    ///
385    /// For example to print and gather all the errors you can do:
386    ///
387    /// ```
388    /// use std::sync::Arc;
389    /// use tokio::sync::Mutex;
390    ///
391    /// # #[tokio::main(flavor = "current_thread")]
392    /// # async fn main() {
393    /// let all_errors = Arc::new(Mutex::new(Vec::new()));
394    ///
395    /// tryhard::retry_fn(|| async {
396    ///     // just some dummy computation that always fails
397    ///     Err::<(), _>("fail")
398    /// })
399    ///     .retries(10)
400    ///     .on_retry(|_attempt, _next_delay, error: &&'static str| {
401    ///         // the future must be `'static` so it cannot contain references
402    ///         let all_errors = Arc::clone(&all_errors);
403    ///         let error = error.clone();
404    ///         async move {
405    ///             eprintln!("Something failed: {}", error);
406    ///             all_errors.lock().await.push(error);
407    ///         }
408    ///     })
409    ///     .await
410    ///     .unwrap_err();
411    ///
412    /// assert_eq!(all_errors.lock().await.len(), 10);
413    /// # }
414    /// ```
415    #[inline]
416    pub fn on_retry<F, OnRetryFut>(self, f: F) -> RetryFuture<MakeFutureT, FutureT, BackoffT, F>
417    where
418        F: Fn(u32, Option<Duration>, &E) -> OnRetryFut,
419    {
420        RetryFuture {
421            make_future: self.make_future,
422            attempts_remaining: self.attempts_remaining,
423            state: self.state,
424            attempt: self.attempt,
425            config: self.config.on_retry(f),
426        }
427    }
428}
429
430/// Configuration describing how to retry a future.
431///
432/// This is useful if you have many futures you want to retry in the same way.
433#[derive(Clone, Copy, PartialEq, Eq)]
434pub struct RetryFutureConfig<BackoffT, OnRetryT> {
435    backoff_strategy: BackoffT,
436    max_delay: Option<Duration>,
437    on_retry: Option<OnRetryT>,
438    max_retries: u32,
439}
440
441impl RetryFutureConfig<NoBackoff, NoOnRetry> {
442    /// Create a new configuration with a max number of retries and no backoff strategy.
443    pub fn new(max_retries: u32) -> Self {
444        Self {
445            backoff_strategy: NoBackoff,
446            max_delay: None,
447            on_retry: None::<NoOnRetry>,
448            max_retries,
449        }
450    }
451}
452
453impl<BackoffT, OnRetryT> RetryFutureConfig<BackoffT, OnRetryT> {
454    /// Set the max duration to sleep between each attempt.
455    #[inline]
456    pub fn max_delay(mut self, delay: Duration) -> Self {
457        self.max_delay = Some(delay);
458        self
459    }
460
461    /// Remove the backoff strategy.
462    ///
463    /// This will make the future be retried immediately without any delay in between attempts.
464    #[inline]
465    pub fn no_backoff(self) -> RetryFutureConfig<NoBackoff, OnRetryT> {
466        self.custom_backoff(NoBackoff)
467    }
468
469    /// Use exponential backoff for retrying the future.
470    ///
471    /// The first delay will be `initial_delay` and afterwards the delay will double every time.
472    #[inline]
473    pub fn exponential_backoff(
474        self,
475        initial_delay: Duration,
476    ) -> RetryFutureConfig<ExponentialBackoff, OnRetryT> {
477        self.custom_backoff(ExponentialBackoff {
478            delay: initial_delay,
479        })
480    }
481
482    /// Use a fixed backoff for retrying the future.
483    ///
484    /// The delay between attempts will always be `delay`.
485    #[inline]
486    pub fn fixed_backoff(self, delay: Duration) -> RetryFutureConfig<FixedBackoff, OnRetryT> {
487        self.custom_backoff(FixedBackoff { delay })
488    }
489
490    /// Use a linear backoff for retrying the future.
491    ///
492    /// The delay will be `delay * attempt` so it'll scale linear with the attempt.
493    #[inline]
494    pub fn linear_backoff(self, delay: Duration) -> RetryFutureConfig<LinearBackoff, OnRetryT> {
495        self.custom_backoff(LinearBackoff { delay })
496    }
497
498    /// Use a custom backoff specified by some function.
499    ///
500    /// See [`RetryFuture::custom_backoff`] for more details.
501    #[inline]
502    pub fn custom_backoff<B>(self, backoff_strategy: B) -> RetryFutureConfig<B, OnRetryT> {
503        RetryFutureConfig {
504            backoff_strategy,
505            max_delay: self.max_delay,
506            max_retries: self.max_retries,
507            on_retry: self.on_retry,
508        }
509    }
510
511    /// Some async computation that will be spawned before each retry.
512    ///
513    /// See [`RetryFuture::on_retry`] for more details.
514    #[inline]
515    pub fn on_retry<F>(self, f: F) -> RetryFutureConfig<BackoffT, F> {
516        RetryFutureConfig {
517            backoff_strategy: self.backoff_strategy,
518            max_delay: self.max_delay,
519            max_retries: self.max_retries,
520            on_retry: Some(f),
521        }
522    }
523}
524
525impl<BackoffT, OnRetryT> fmt::Debug for RetryFutureConfig<BackoffT, OnRetryT>
526where
527    BackoffT: fmt::Debug,
528{
529    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530        f.debug_struct("RetryFutureConfig")
531            .field("backoff_strategy", &self.backoff_strategy)
532            .field("max_delay", &self.max_delay)
533            .field("max_retries", &self.max_retries)
534            .field(
535                "on_retry",
536                &format_args!("<{}>", std::any::type_name::<OnRetryT>()),
537            )
538            .finish()
539    }
540}
541
542pin_project! {
543    #[project = RetryStateProj]
544    #[allow(clippy::large_enum_variant)]
545    enum RetryState<F> {
546        NotStarted,
547        WaitingForFuture { #[pin] future: F },
548        TimerActive { #[pin] sleep: tokio::time::Sleep },
549    }
550}
551
552impl<F, Fut, B, T, E, OnRetryT> Future for RetryFuture<F, Fut, B, OnRetryT>
553where
554    F: FnMut() -> Fut,
555    Fut: Future<Output = Result<T, E>>,
556    for<'a> B: BackoffStrategy<'a, E>,
557    for<'a> <B as BackoffStrategy<'a, E>>::Output: Into<RetryPolicy>,
558    OnRetryT: OnRetry<E>,
559{
560    type Output = Result<T, E>;
561
562    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
563        loop {
564            let this = self.as_mut().project();
565
566            let new_state = match this.state.project() {
567                RetryStateProj::NotStarted => RetryState::WaitingForFuture {
568                    future: (this.make_future)(),
569                },
570
571                RetryStateProj::TimerActive { sleep } => match sleep.poll(cx) {
572                    Poll::Ready(()) => RetryState::WaitingForFuture {
573                        future: (this.make_future)(),
574                    },
575                    Poll::Pending => return Poll::Pending,
576                },
577
578                RetryStateProj::WaitingForFuture { future } => match future.poll(cx) {
579                    Poll::Pending => return Poll::Pending,
580                    Poll::Ready(Ok(value)) => {
581                        return Poll::Ready(Ok(value));
582                    }
583                    Poll::Ready(Err(error)) => {
584                        if *this.attempts_remaining == 0 {
585                            if let Some(on_retry) = &mut this.config.on_retry {
586                                tokio::spawn(on_retry.on_retry(*this.attempt, None, &error));
587                            }
588
589                            return Poll::Ready(Err(error));
590                        } else {
591                            *this.attempt += 1;
592                            *this.attempts_remaining -= 1;
593
594                            let delay: RetryPolicy = this
595                                .config
596                                .backoff_strategy
597                                .delay(*this.attempt, &error)
598                                .into();
599                            let mut delay_duration = match delay {
600                                RetryPolicy::Delay(duration) => duration,
601                                RetryPolicy::Break => {
602                                    if let Some(on_retry) = &mut this.config.on_retry {
603                                        tokio::spawn(on_retry.on_retry(
604                                            *this.attempt,
605                                            None,
606                                            &error,
607                                        ));
608                                    }
609
610                                    return Poll::Ready(Err(error));
611                                }
612                            };
613
614                            if let Some(max_delay) = this.config.max_delay {
615                                delay_duration = delay_duration.min(max_delay);
616                            }
617
618                            if let Some(on_retry) = &mut this.config.on_retry {
619                                tokio::spawn(on_retry.on_retry(
620                                    *this.attempt,
621                                    Some(delay_duration),
622                                    &error,
623                                ));
624                            }
625
626                            let sleep = tokio::time::sleep(delay_duration);
627
628                            RetryState::TimerActive { sleep }
629                        }
630                    }
631                },
632            };
633
634            self.as_mut().project().state.set(new_state);
635        }
636    }
637}
638
639/// What to do when a future returns an error. Used with [`RetryFuture::custom`].
640///
641/// [`RetryFuture::custom`]: struct.RetryFuture.html#method.custom_backoff
642#[derive(Debug, Eq, PartialEq, Clone)]
643pub enum RetryPolicy {
644    /// Try again in the specified `Duration`.
645    Delay(Duration),
646
647    /// Don't retry.
648    Break,
649}
650
651impl From<Duration> for RetryPolicy {
652    fn from(duration: Duration) -> Self {
653        RetryPolicy::Delay(duration)
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660    use std::sync::atomic::{AtomicUsize, Ordering};
661    use std::sync::Arc;
662    use std::{convert::Infallible, time::Instant};
663
664    #[tokio::test]
665    async fn succeed() {
666        retry_fn(|| async { Ok::<_, Infallible>(true) })
667            .retries(10)
668            .await
669            .unwrap();
670    }
671
672    #[tokio::test]
673    async fn retrying_correct_amount_of_times() {
674        let counter = AtomicUsize::new(0);
675
676        let err = retry_fn(|| async {
677            counter.fetch_add(1, Ordering::SeqCst);
678            Err::<Infallible, _>("error")
679        })
680        .retries(10)
681        .await
682        .unwrap_err();
683
684        assert_eq!(err, "error");
685        assert_eq!(counter.load(Ordering::Relaxed), 11);
686    }
687
688    #[tokio::test]
689    async fn retry_0_times() {
690        let counter = AtomicUsize::new(0);
691
692        retry_fn(|| async {
693            counter.fetch_add(1, Ordering::SeqCst);
694            Err::<Infallible, _>("error")
695        })
696        .retries(0)
697        .await
698        .unwrap_err();
699
700        assert_eq!(counter.load(Ordering::Relaxed), 1);
701    }
702
703    #[tokio::test]
704    async fn the_backoff_strategy_gets_used() {
705        async fn make_future() -> Result<Infallible, &'static str> {
706            Err("foo")
707        }
708
709        let start = Instant::now();
710        retry_fn(make_future)
711            .retries(10)
712            .no_backoff()
713            .await
714            .unwrap_err();
715        let time_with_none = start.elapsed();
716
717        let start = Instant::now();
718        retry_fn(make_future)
719            .retries(10)
720            .fixed_backoff(Duration::from_millis(10))
721            .await
722            .unwrap_err();
723        let time_with_fixed = start.elapsed();
724
725        // assertions about what the exact times are are very finicky so lets just assert that the
726        // one without backoff is slower.
727        assert!(time_with_fixed >= time_with_none);
728    }
729
730    // `RetryFuture` must be `Send` to be used with `async_trait`
731    // Generally we also want our futures to be `Send`
732    #[test]
733    fn is_send() {
734        fn assert_send<T: Send>(_: T) {}
735        async fn some_future() -> Result<(), Infallible> {
736            Ok(())
737        }
738        assert_send(retry_fn(some_future).retries(10));
739    }
740
741    #[tokio::test]
742    async fn stop_retrying() {
743        let mut n = 0;
744        let make_future = || {
745            n += 1;
746            if n == 8 {
747                panic!("retried too many times");
748            }
749            async { Err::<Infallible, _>("foo") }
750        };
751
752        let error = retry_fn(make_future)
753            .retries(10)
754            .custom_backoff(|n, _: &&'static str| {
755                if n >= 3 {
756                    RetryPolicy::Break
757                } else {
758                    RetryPolicy::Delay(Duration::from_nanos(10))
759                }
760            })
761            .await
762            .unwrap_err();
763
764        assert_eq!(error, "foo");
765    }
766
767    #[tokio::test]
768    async fn custom_returning_duration() {
769        retry_fn(|| async { Ok::<_, Infallible>(true) })
770            .retries(10)
771            .custom_backoff(|_, _: &Infallible| Duration::from_nanos(10))
772            .await
773            .unwrap();
774    }
775
776    #[tokio::test]
777    async fn retry_hook_succeed() {
778        use std::sync::Arc;
779        use tokio::sync::Mutex;
780
781        let errors = Arc::new(Mutex::new(Vec::new()));
782
783        retry_fn(|| async { Err::<Infallible, String>("error".to_string()) })
784            .retries(10)
785            .on_retry(|attempt, next_delay, error: &String| {
786                let errors = Arc::clone(&errors);
787                let error = error.clone();
788                async move {
789                    errors.lock().await.push((attempt, next_delay, error));
790                }
791            })
792            .await
793            .unwrap_err();
794
795        let errors = errors.lock().await;
796        assert_eq!(errors.len(), 10);
797        for n in 1_u32..=10 {
798            assert_eq!(
799                &errors[(n - 1) as usize],
800                &(n, Some(Duration::new(0, 0)), "error".to_string())
801            );
802        }
803    }
804
805    #[tokio::test]
806    async fn reusing_the_config() {
807        let counter = Arc::new(AtomicUsize::new(0));
808
809        let config = RetryFutureConfig::new(10)
810            .linear_backoff(Duration::from_millis(10))
811            .on_retry(|_, _, _: &&'static str| {
812                let counter = Arc::clone(&counter);
813                async move {
814                    counter.fetch_add(1, Ordering::SeqCst);
815                }
816            });
817
818        let ok_value = retry_fn(|| async { Ok::<_, &str>(true) })
819            .with_config(config)
820            .await
821            .unwrap();
822        assert!(ok_value);
823        assert_eq!(counter.load(Ordering::SeqCst), 0);
824
825        let err_value = retry_fn(|| async { Err::<(), _>("foo") })
826            .with_config(config)
827            .await
828            .unwrap_err();
829        assert_eq!(err_value, "foo");
830        assert_eq!(counter.load(Ordering::SeqCst), 10);
831    }
832
833    #[tokio::test]
834    async fn custom_backoff_wrapping_another_strategy() {
835        #[derive(Clone)]
836        struct MyBackoffStrategy {
837            inner: ExponentialBackoff,
838        }
839
840        impl<'a> BackoffStrategy<'a, std::io::Error> for MyBackoffStrategy {
841            type Output = RetryPolicy;
842
843            fn delay(&mut self, attempt: u32, error: &'a std::io::Error) -> Self::Output {
844                if error.kind() == std::io::ErrorKind::NotFound {
845                    RetryPolicy::Break
846                } else {
847                    RetryPolicy::Delay(self.inner.delay(attempt, error))
848                }
849            }
850        }
851
852        #[derive(Clone)]
853        struct MyOnRetry;
854
855        impl OnRetry<std::io::Error> for MyOnRetry {
856            type Future = futures::future::BoxFuture<'static, ()>;
857
858            fn on_retry(
859                &mut self,
860                attempt: u32,
861                next_delay: Option<Duration>,
862                previous_error: &std::io::Error,
863            ) -> Self::Future {
864                let previous_error = previous_error.to_string();
865                Box::pin(async move {
866                    println!("{} {:?} {}", attempt, next_delay, previous_error);
867                })
868            }
869        }
870
871        let config: RetryFutureConfig<MyBackoffStrategy, MyOnRetry> = RetryFutureConfig::new(10)
872            .custom_backoff(MyBackoffStrategy {
873                inner: ExponentialBackoff::new(Duration::from_millis(10)),
874            })
875            .on_retry(MyOnRetry);
876
877        retry_fn(|| async { Ok::<_, std::io::Error>(true) })
878            .with_config(config.clone())
879            .await
880            .unwrap();
881
882        retry_fn(|| async { Ok::<_, std::io::Error>(true) })
883            .with_config(config)
884            .await
885            .unwrap();
886    }
887
888    #[tokio::test]
889    async fn inference_works() {
890        // See https://github.com/EmbarkStudios/tryhard/issues/20 for details.
891        std::mem::drop(async {
892            let _ = retry_fn(|| async { Result::<_, Infallible>::Ok(()) })
893                .retries(0)
894                .on_retry(|_, _, _| async {})
895                .await;
896        });
897    }
898}