backon-1.2.0/.cargo_vcs_info.json0000644000000001440000000000100122700ustar { "git": { "sha1": "50ce18569fe94dd3f6d285a8f82c2b0ebce2b1c6" }, "path_in_vcs": "backon" }backon-1.2.0/Cargo.toml0000644000000042550000000000100102750ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70" name = "backon" version = "1.2.0" build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Make retry like a built-in feature provided by Rust." documentation = "https://docs.rs/backon" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/Xuanwo/backon" [package.metadata.docs.rs] all-features = true targets = [ "x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "wasm32-unknown-unknown", ] [lib] name = "backon" path = "src/lib.rs" [dependencies.fastrand] version = "2" [dev-dependencies.anyhow] version = "1" [dev-dependencies.reqwest] version = "0.12" [dev-dependencies.spin] version = "0.9.8" [features] default = [ "std-blocking-sleep", "tokio-sleep", "gloo-timers-sleep", ] gloo-timers-sleep = [ "dep:gloo-timers", "gloo-timers?/futures", ] std-blocking-sleep = [] tokio-sleep = [ "dep:tokio", "tokio?/time", ] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "1" optional = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.sqlx] version = "0.8.0" features = [ "runtime-tokio", "sqlite", ] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.tokio] version = "1" features = [ "time", "rt", "macros", "sync", "rt-multi-thread", ] [target.'cfg(target_arch = "wasm32")'.dependencies.gloo-timers] version = "0.3" optional = true [target.'cfg(target_arch = "wasm32")'.dev-dependencies.tokio] version = "1" features = [ "macros", "rt", "sync", ] default-features = false [target.'cfg(target_arch = "wasm32")'.dev-dependencies.wasm-bindgen-test] version = "0.3" backon-1.2.0/Cargo.toml.orig000064400000000000000000000025741046102023000137600ustar 00000000000000[package] description = "Make retry like a built-in feature provided by Rust." documentation = "https://docs.rs/backon" name = "backon" readme = "../README.md" rust-version = "1.70" version = "1.2.0" edition.workspace = true license.workspace = true repository.workspace = true [package.metadata.docs.rs] all-features = true targets = [ "x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "wasm32-unknown-unknown", ] [features] default = ["std-blocking-sleep", "tokio-sleep", "gloo-timers-sleep"] std-blocking-sleep = [] gloo-timers-sleep = ["dep:gloo-timers", "gloo-timers?/futures"] tokio-sleep = ["dep:tokio", "tokio?/time"] [dependencies] fastrand = "2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers = { version = "0.3", optional = true } [dev-dependencies] anyhow = "1" reqwest = "0.12" spin = "0.9.8" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] tokio = { version = "1", features = [ "macros", "rt", "sync", ], default-features = false } wasm-bindgen-test = "0.3" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] sqlx = { version = "0.8.0", features = ["runtime-tokio", "sqlite"] } tokio = { version = "1", features = [ "time", "rt", "macros", "sync", "rt-multi-thread", ] } backon-1.2.0/README.md000064400000000000000000000057611046102023000123510ustar 00000000000000# BackON   [![Build Status]][actions] [![Latest Version]][crates.io] [![](https://img.shields.io/discord/1111711408875393035?logo=discord&label=discord)](https://discord.gg/8ARnvtJePD) [Build Status]: https://img.shields.io/github/actions/workflow/status/Xuanwo/backon/ci.yml?branch=main [actions]: https://github.com/Xuanwo/backon/actions?query=branch%3Amain [Latest Version]: https://img.shields.io/crates/v/backon.svg [crates.io]: https://crates.io/crates/backon BackON Make **retry** like a built-in feature provided by Rust. - **Simple**: Just like a built-in feature: `your_fn.retry(ExponentialBuilder::default()).await`. - **Flexible**: Supports both blocking and async functions. - **Powerful**: Allows control over retry behavior such as [`when`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.when) and [`notify`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.notify). - **Customizable**: Supports custom retry strategies like [exponential](https://docs.rs/backon/latest/backon/struct.ExponentialBuilder.html), [constant](https://docs.rs/backon/latest/backon/struct.ConstantBuilder.html), etc. --- ## Quick Start ### Retry an async function. ```rust use anyhow::Result; use backon::ExponentialBuilder; use backon::Retryable; async fn fetch() -> Result { Ok("hello, world!".to_string()) } #[tokio::main] async fn main() -> Result<()> { let content = fetch // Retry with exponential backoff .retry(ExponentialBuilder::default()) // Sleep implementation, required if no feature has been enabled .sleep(tokio::time::sleep) // When to retry .when(|e| e.to_string() == "EOF") // Notify when retrying .notify(|err: &anyhow::Error, dur: Duration| { println!("retrying {:?} after {:?}", err, dur); }) .await?; println!("fetch succeeded: {}", content); Ok(()) } ``` ### Retry a blocking function. ```rust use anyhow::Result; use backon::BlockingRetryable; use backon::ExponentialBuilder; fn fetch() -> Result { Ok("hello, world!".to_string()) } fn main() -> Result<()> { let content = fetch // Retry with exponential backoff .retry(ExponentialBuilder::default()) // When to retry .when(|e| e.to_string() == "EOF") // Notify when retrying .notify(|err: &anyhow::Error, dur: Duration| { println!("retrying {:?} after {:?}", err, dur); }) .call()?; println!("fetch succeeded: {}", content); Ok(()) } ``` ## Contributing Check out the [CONTRIBUTING.md](./CONTRIBUTING.md) guide for more details on getting started with contributing to this project. ## Getting help Submit [issues](https://github.com/Xuanwo/backon/issues/new/choose) for bug report or asking questions in [discussion](https://github.com/Xuanwo/backon/discussions/new?category=q-a). ## License Licensed under Apache License, Version 2.0. backon-1.2.0/src/backoff/api.rs000064400000000000000000000013731046102023000143660ustar 00000000000000use core::fmt::Debug; use core::time::Duration; /// BackoffBuilder is utilized to construct a new backoff. pub trait BackoffBuilder: Debug + Send + Sync + Unpin { /// The associated backoff returned by this builder. type Backoff: Backoff; /// Construct a new backoff using the builder. fn build(self) -> Self::Backoff; } /// Backoff is an [`Iterator`] that returns [`Duration`]. /// /// - `Some(Duration)` indicates the caller should `sleep(Duration)` and retry the request. /// - `None` indicates the limits have been reached, and the caller should return the current error instead. pub trait Backoff: Iterator + Send + Sync + Unpin {} impl Backoff for T where T: Iterator + Debug + Send + Sync + Unpin {} backon-1.2.0/src/backoff/constant.rs000064400000000000000000000102521046102023000154420ustar 00000000000000use core::time::Duration; use crate::backoff::BackoffBuilder; /// ConstantBuilder is used to create a [`ConstantBackoff`], providing a steady delay with a fixed number of retries. /// /// # Default /// /// - delay: 1s /// - max_times: 3 /// /// # Examples /// /// ```no_run /// use anyhow::Result; /// use backon::ConstantBuilder; /// use backon::Retryable; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch.retry(ConstantBuilder::default()).await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` #[derive(Debug, Clone, Copy)] pub struct ConstantBuilder { delay: Duration, max_times: Option, jitter: bool, } impl Default for ConstantBuilder { fn default() -> Self { Self { delay: Duration::from_secs(1), max_times: Some(3), jitter: false, } } } impl ConstantBuilder { /// Set the delay for the backoff. pub fn with_delay(mut self, delay: Duration) -> Self { self.delay = delay; self } /// Set the maximum duration for the backoff. pub fn with_max_times(mut self, max_times: usize) -> Self { self.max_times = Some(max_times); self } /// Set jitter for the backoff. /// /// Jitter is a random value added to the delay to prevent a thundering herd problem. pub fn with_jitter(mut self) -> Self { self.jitter = true; self } } impl BackoffBuilder for ConstantBuilder { type Backoff = ConstantBackoff; fn build(self) -> Self::Backoff { ConstantBackoff { delay: self.delay, max_times: self.max_times, attempts: 0, jitter: self.jitter, } } } /// ConstantBackoff offers a consistent delay with a limited number of retries. /// /// This backoff strategy is constructed by [`ConstantBuilder`]. #[doc(hidden)] #[derive(Debug)] pub struct ConstantBackoff { delay: Duration, max_times: Option, attempts: usize, jitter: bool, } impl Iterator for ConstantBackoff { type Item = Duration; fn next(&mut self) -> Option { let delay = || match self.jitter { true => self.delay + self.delay.mul_f32(fastrand::f32()), false => self.delay, }; match self.max_times { None => Some(delay()), Some(max_times) => { if self.attempts >= max_times { None } else { self.attempts += 1; Some(delay()) } } } } } #[cfg(test)] mod tests { use core::time::Duration; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test as test; use crate::BackoffBuilder; use crate::ConstantBuilder; #[test] fn test_constant_default() { let mut exp = ConstantBuilder::default().build(); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_constant_with_delay() { let mut exp = ConstantBuilder::default() .with_delay(Duration::from_secs(2)) .build(); assert_eq!(Some(Duration::from_secs(2)), exp.next()); assert_eq!(Some(Duration::from_secs(2)), exp.next()); assert_eq!(Some(Duration::from_secs(2)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_constant_with_times() { let mut exp = ConstantBuilder::default().with_max_times(1).build(); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_constant_with_jitter() { let mut it = ConstantBuilder::default().with_jitter().build(); let dur = it.next().unwrap(); fastrand::seed(7); assert!(dur > Duration::from_secs(1)); } } backon-1.2.0/src/backoff/exponential.rs000064400000000000000000000224761046102023000161520ustar 00000000000000use core::time::Duration; use crate::backoff::BackoffBuilder; /// ExponentialBuilder is used to construct an [`ExponentialBackoff`] that offers delays with exponential retries. /// /// # Default /// /// - jitter: false /// - factor: 2 /// - min_delay: 1s /// - max_delay: 60s /// - max_times: 3 /// /// # Examples /// /// ```no_run /// use anyhow::Result; /// use backon::ExponentialBuilder; /// use backon::Retryable; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch.retry(ExponentialBuilder::default()).await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` #[derive(Debug, Clone, Copy)] pub struct ExponentialBuilder { jitter: bool, factor: f32, min_delay: Duration, max_delay: Option, max_times: Option, } impl Default for ExponentialBuilder { fn default() -> Self { Self { jitter: false, factor: 2.0, min_delay: Duration::from_secs(1), max_delay: Some(Duration::from_secs(60)), max_times: Some(3), } } } impl ExponentialBuilder { /// Set the jitter for the backoff. /// /// When jitter is enabled, [`ExponentialBackoff`] will add a random jitter within `(0, min_delay)` /// to the current delay. pub fn with_jitter(mut self) -> Self { self.jitter = true; self } /// Set the factor for the backoff. /// /// # Panics /// /// This function will panic if the input factor is less than `1.0`. pub fn with_factor(mut self, factor: f32) -> Self { debug_assert!(factor >= 1.0, "invalid factor that lower than 1"); self.factor = factor; self } /// Set the minimum delay for the backoff. pub fn with_min_delay(mut self, min_delay: Duration) -> Self { self.min_delay = min_delay; self } /// Set the maximum delay for the backoff. /// /// The delay will not increase if the current delay exceeds the maximum delay. pub fn with_max_delay(mut self, max_delay: Duration) -> Self { self.max_delay = Some(max_delay); self } /// Set the maximum number of attempts for the current backoff. /// /// The backoff will stop if the maximum number of attempts is reached. pub fn with_max_times(mut self, max_times: usize) -> Self { self.max_times = Some(max_times); self } } impl BackoffBuilder for ExponentialBuilder { type Backoff = ExponentialBackoff; fn build(self) -> Self::Backoff { ExponentialBackoff { jitter: self.jitter, factor: self.factor, min_delay: self.min_delay, max_delay: self.max_delay, max_times: self.max_times, current_delay: None, attempts: 0, } } } /// ExponentialBackoff provides a delay with exponential retries. /// /// This backoff strategy is constructed by [`ExponentialBuilder`]. #[doc(hidden)] #[derive(Debug)] pub struct ExponentialBackoff { jitter: bool, factor: f32, min_delay: Duration, max_delay: Option, max_times: Option, current_delay: Option, attempts: usize, } impl Iterator for ExponentialBackoff { type Item = Duration; fn next(&mut self) -> Option { if self.attempts >= self.max_times.unwrap_or(usize::MAX) { return None; } self.attempts += 1; let mut tmp_cur = match self.current_delay { None => { // If current_delay is None, it's must be the first time to retry. self.current_delay = Some(self.min_delay); self.min_delay } Some(mut cur) => { // If current delay larger than max delay, we should stop increment anymore. if let Some(max_delay) = self.max_delay { if cur < max_delay { cur = saturating_mul(cur, self.factor); } if cur > max_delay { cur = max_delay; } } else { cur = saturating_mul(cur, self.factor); } self.current_delay = Some(cur); cur } }; // If jitter is enabled, add random jitter based on min delay. if self.jitter { tmp_cur = tmp_cur.saturating_add(self.min_delay.mul_f32(fastrand::f32())); } Some(tmp_cur) } } #[inline] pub(crate) fn saturating_mul(d: Duration, rhs: f32) -> Duration { match Duration::try_from_secs_f32(rhs * d.as_secs_f32()) { Ok(v) => v, Err(_) => Duration::MAX, } } #[cfg(test)] mod tests { use core::time::Duration; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test as test; use crate::BackoffBuilder; use crate::ExponentialBuilder; #[test] fn test_exponential_default() { let mut exp = ExponentialBuilder::default().build(); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(Some(Duration::from_secs(2)), exp.next()); assert_eq!(Some(Duration::from_secs(4)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_exponential_factor() { let mut exp = ExponentialBuilder::default().with_factor(1.5).build(); assert_eq!(Some(Duration::from_secs_f32(1.0)), exp.next()); assert_eq!(Some(Duration::from_secs_f32(1.5)), exp.next()); assert_eq!(Some(Duration::from_secs_f32(2.25)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_exponential_jitter() { let mut exp = ExponentialBuilder::default().with_jitter().build(); let v = exp.next().expect("value must valid"); assert!(v >= Duration::from_secs(1), "current: {v:?}"); assert!(v < Duration::from_secs(2), "current: {v:?}"); let v = exp.next().expect("value must valid"); assert!(v >= Duration::from_secs(2), "current: {v:?}"); assert!(v < Duration::from_secs(4), "current: {v:?}"); let v = exp.next().expect("value must valid"); assert!(v >= Duration::from_secs(4), "current: {v:?}"); assert!(v < Duration::from_secs(8), "current: {v:?}"); assert_eq!(None, exp.next()); } #[test] fn test_exponential_min_delay() { let mut exp = ExponentialBuilder::default() .with_min_delay(Duration::from_millis(500)) .build(); assert_eq!(Some(Duration::from_millis(500)), exp.next()); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(Some(Duration::from_secs(2)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_exponential_max_delay_with_default() { let mut exp = ExponentialBuilder::default() .with_max_delay(Duration::from_secs(2)) .build(); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(Some(Duration::from_secs(2)), exp.next()); assert_eq!(Some(Duration::from_secs(2)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_exponential_max_delay_without_default_1() { let mut exp = ExponentialBuilder { jitter: false, factor: 10_000_000_000_f32, min_delay: Duration::from_secs(1), max_delay: None, max_times: None, } .build(); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next()); assert_eq!(Some(Duration::MAX), exp.next()); assert_eq!(Some(Duration::MAX), exp.next()); } #[test] fn test_exponential_max_delay_without_default_2() { let mut exp = ExponentialBuilder { jitter: true, factor: 10_000_000_000_f32, min_delay: Duration::from_secs(10_000_000_000), max_delay: None, max_times: Some(2), } .build(); let v = exp.next().expect("value must valid"); assert!(v >= Duration::from_secs(10_000_000_000), "current: {v:?}"); assert!(v < Duration::from_secs(20_000_000_000), "current: {v:?}"); assert_eq!(Some(Duration::MAX), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_exponential_max_delay_without_default_3() { let mut exp = ExponentialBuilder { jitter: false, factor: 10_000_000_000_f32, min_delay: Duration::from_secs(10_000_000_000), max_delay: Some(Duration::from_secs(60_000_000_000)), max_times: Some(3), } .build(); assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next()); assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next()); assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next()); assert_eq!(None, exp.next()); } #[test] fn test_exponential_max_times() { let mut exp = ExponentialBuilder::default().with_max_times(1).build(); assert_eq!(Some(Duration::from_secs(1)), exp.next()); assert_eq!(None, exp.next()); } } backon-1.2.0/src/backoff/fibonacci.rs000064400000000000000000000160631046102023000155340ustar 00000000000000use core::time::Duration; use crate::backoff::BackoffBuilder; /// FibonacciBuilder is used to build a [`FibonacciBackoff`] which offers a delay with Fibonacci-based retries. /// /// # Default /// /// - jitter: false /// - min_delay: 1s /// - max_delay: 60s /// - max_times: 3 /// /// # Examples /// /// ```no_run /// use anyhow::Result; /// use backon::FibonacciBuilder; /// use backon::Retryable; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch.retry(FibonacciBuilder::default()).await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` #[derive(Debug, Clone, Copy)] pub struct FibonacciBuilder { jitter: bool, min_delay: Duration, max_delay: Option, max_times: Option, } impl Default for FibonacciBuilder { fn default() -> Self { Self { jitter: false, min_delay: Duration::from_secs(1), max_delay: Some(Duration::from_secs(60)), max_times: Some(3), } } } impl FibonacciBuilder { /// Set the jitter for the backoff. /// /// When jitter is enabled, FibonacciBackoff will add a random jitter between `(0, min_delay)` to the delay. pub fn with_jitter(mut self) -> Self { self.jitter = true; self } /// Set the minimum delay for the backoff. pub fn with_min_delay(mut self, min_delay: Duration) -> Self { self.min_delay = min_delay; self } /// Set the maximum delay for the current backoff. /// /// The delay will not increase if the current delay exceeds the maximum delay. pub fn with_max_delay(mut self, max_delay: Duration) -> Self { self.max_delay = Some(max_delay); self } /// Set the maximum number of attempts for the current backoff. /// /// The backoff will stop if the maximum number of attempts is reached. pub fn with_max_times(mut self, max_times: usize) -> Self { self.max_times = Some(max_times); self } } impl BackoffBuilder for FibonacciBuilder { type Backoff = FibonacciBackoff; fn build(self) -> Self::Backoff { FibonacciBackoff { jitter: self.jitter, min_delay: self.min_delay, max_delay: self.max_delay, max_times: self.max_times, previous_delay: None, current_delay: None, attempts: 0, } } } /// FibonacciBackoff offers a delay with Fibonacci-based retries. /// /// This backoff strategy is constructed by [`FibonacciBuilder`]. #[doc(hidden)] #[derive(Debug)] pub struct FibonacciBackoff { jitter: bool, min_delay: Duration, max_delay: Option, max_times: Option, previous_delay: Option, current_delay: Option, attempts: usize, } impl Iterator for FibonacciBackoff { type Item = Duration; fn next(&mut self) -> Option { if self.attempts >= self.max_times.unwrap_or(usize::MAX) { return None; } self.attempts += 1; match self.current_delay { None => { // If current_delay is None, it's must be the first time to retry. let mut next = self.min_delay; self.current_delay = Some(next); // If jitter is enabled, add random jitter based on min delay. if self.jitter { next += self.min_delay.mul_f32(fastrand::f32()); } Some(next) } Some(cur) => { let mut next = cur; // If current delay larger than max delay, we should stop increment anymore. if next < self.max_delay.unwrap_or(Duration::MAX) { if let Some(prev) = self.previous_delay { next += prev; self.current_delay = Some(next); } self.previous_delay = Some(cur); } // If jitter is enabled, add random jitter based on min delay. if self.jitter { next += self.min_delay.mul_f32(fastrand::f32()); } Some(next) } } } } #[cfg(test)] mod tests { use core::time::Duration; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test as test; use crate::BackoffBuilder; use crate::FibonacciBuilder; #[test] fn test_fibonacci_default() { let mut fib = FibonacciBuilder::default().build(); assert_eq!(Some(Duration::from_secs(1)), fib.next()); assert_eq!(Some(Duration::from_secs(1)), fib.next()); assert_eq!(Some(Duration::from_secs(2)), fib.next()); assert_eq!(None, fib.next()); } #[test] fn test_fibonacci_jitter() { let mut fib = FibonacciBuilder::default().with_jitter().build(); let v = fib.next().expect("value must valid"); assert!(v >= Duration::from_secs(1), "current: {v:?}"); assert!(v < Duration::from_secs(2), "current: {v:?}"); let v = fib.next().expect("value must valid"); assert!(v >= Duration::from_secs(1), "current: {v:?}"); assert!(v < Duration::from_secs(2), "current: {v:?}"); let v = fib.next().expect("value must valid"); assert!(v >= Duration::from_secs(2), "current: {v:?}"); assert!(v < Duration::from_secs(3), "current: {v:?}"); assert_eq!(None, fib.next()); } #[test] fn test_fibonacci_min_delay() { let mut fib = FibonacciBuilder::default() .with_min_delay(Duration::from_millis(500)) .build(); assert_eq!(Some(Duration::from_millis(500)), fib.next()); assert_eq!(Some(Duration::from_millis(500)), fib.next()); assert_eq!(Some(Duration::from_secs(1)), fib.next()); assert_eq!(None, fib.next()); } #[test] fn test_fibonacci_max_delay() { let mut fib = FibonacciBuilder::default() .with_max_times(4) .with_max_delay(Duration::from_secs(2)) .build(); assert_eq!(Some(Duration::from_secs(1)), fib.next()); assert_eq!(Some(Duration::from_secs(1)), fib.next()); assert_eq!(Some(Duration::from_secs(2)), fib.next()); assert_eq!(Some(Duration::from_secs(2)), fib.next()); assert_eq!(None, fib.next()); } #[test] fn test_fibonacci_max_times() { let mut fib = FibonacciBuilder::default().with_max_times(6).build(); assert_eq!(Some(Duration::from_secs(1)), fib.next()); assert_eq!(Some(Duration::from_secs(1)), fib.next()); assert_eq!(Some(Duration::from_secs(2)), fib.next()); assert_eq!(Some(Duration::from_secs(3)), fib.next()); assert_eq!(Some(Duration::from_secs(5)), fib.next()); assert_eq!(Some(Duration::from_secs(8)), fib.next()); assert_eq!(None, fib.next()); } } backon-1.2.0/src/backoff/mod.rs000064400000000000000000000004541046102023000143730ustar 00000000000000mod api; pub use api::*; mod constant; pub use constant::ConstantBackoff; pub use constant::ConstantBuilder; mod fibonacci; pub use fibonacci::FibonacciBackoff; pub use fibonacci::FibonacciBuilder; mod exponential; pub use exponential::ExponentialBackoff; pub use exponential::ExponentialBuilder; backon-1.2.0/src/blocking_retry.rs000064400000000000000000000233351046102023000152410ustar 00000000000000use core::time::Duration; use crate::backoff::BackoffBuilder; use crate::blocking_sleep::MaybeBlockingSleeper; use crate::{Backoff, BlockingSleeper, DefaultBlockingSleeper}; /// BlockingRetryable adds retry support for blocking functions. /// /// For example: /// /// - Functions without extra args: /// /// ```ignore /// fn fetch() -> Result { /// Ok("hello, world!".to_string()) /// } /// ``` /// /// - Closures /// /// ```ignore /// || { /// Ok("hello, world!".to_string()) /// } /// ``` /// /// # Example /// /// ```no_run /// use anyhow::Result; /// use backon::BlockingRetryable; /// use backon::ExponentialBuilder; /// /// fn fetch() -> Result { /// Ok("hello, world!".to_string()) /// } /// /// fn main() -> Result<()> { /// let content = fetch.retry(ExponentialBuilder::default()).call()?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub trait BlockingRetryable Result> { /// Generate a new retry. fn retry(self, builder: B) -> BlockingRetry; } impl BlockingRetryable for F where B: BackoffBuilder, F: FnMut() -> Result, { fn retry(self, builder: B) -> BlockingRetry { BlockingRetry::new(self, builder.build()) } } /// Retry structure generated by [`BlockingRetryable`]. pub struct BlockingRetry< B: Backoff, T, E, F: FnMut() -> Result, SF: MaybeBlockingSleeper = DefaultBlockingSleeper, RF = fn(&E) -> bool, NF = fn(&E, Duration), > { backoff: B, retryable: RF, notify: NF, f: F, sleep_fn: SF, } impl BlockingRetry where B: Backoff, F: FnMut() -> Result, { /// Create a new retry. fn new(f: F, backoff: B) -> Self { BlockingRetry { backoff, retryable: |_: &E| true, notify: |_: &E, _: Duration| {}, sleep_fn: DefaultBlockingSleeper::default(), f, } } } impl BlockingRetry where B: Backoff, F: FnMut() -> Result, SF: MaybeBlockingSleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { /// Set the sleeper for retrying. /// /// The sleeper should implement the [`BlockingSleeper`] trait. The simplest way is to use a closure like `Fn(Duration)`. /// /// If not specified, we use the [`DefaultBlockingSleeper`]. /// /// # Examples /// /// ```no_run /// use anyhow::Result; /// use backon::BlockingRetryable; /// use backon::ExponentialBuilder; /// /// fn fetch() -> Result { /// Ok("hello, world!".to_string()) /// } /// /// fn main() -> Result<()> { /// let retry = fetch /// .retry(ExponentialBuilder::default()) /// .sleep(std::thread::sleep); /// let content = retry.call()?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn sleep(self, sleep_fn: SN) -> BlockingRetry { BlockingRetry { backoff: self.backoff, retryable: self.retryable, notify: self.notify, f: self.f, sleep_fn, } } /// Set the conditions for retrying. /// /// If not specified, all errors are considered retryable. /// /// # Examples /// /// ```no_run /// use anyhow::Result; /// use backon::BlockingRetryable; /// use backon::ExponentialBuilder; /// /// fn fetch() -> Result { /// Ok("hello, world!".to_string()) /// } /// /// fn main() -> Result<()> { /// let retry = fetch /// .retry(ExponentialBuilder::default()) /// .when(|e| e.to_string() == "EOF"); /// let content = retry.call()?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn when bool>( self, retryable: RN, ) -> BlockingRetry { BlockingRetry { backoff: self.backoff, retryable, notify: self.notify, f: self.f, sleep_fn: self.sleep_fn, } } /// Set to notify for all retry attempts. /// /// When a retry happens, the input function will be invoked with the error and the sleep duration before pausing. /// /// If not specified, this operation does nothing. /// /// # Examples /// /// ```no_run /// use core::time::Duration; /// /// use anyhow::Result; /// use backon::BlockingRetryable; /// use backon::ExponentialBuilder; /// /// fn fetch() -> Result { /// Ok("hello, world!".to_string()) /// } /// /// fn main() -> Result<()> { /// let retry = fetch.retry(ExponentialBuilder::default()).notify( /// |err: &anyhow::Error, dur: Duration| { /// println!("retrying error {:?} with sleeping {:?}", err, dur); /// }, /// ); /// let content = retry.call()?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn notify( self, notify: NN, ) -> BlockingRetry { BlockingRetry { backoff: self.backoff, retryable: self.retryable, notify, f: self.f, sleep_fn: self.sleep_fn, } } } impl BlockingRetry where B: Backoff, F: FnMut() -> Result, SF: BlockingSleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { /// Call the retried function. /// /// TODO: implement [`FnOnce`] after it stable. pub fn call(mut self) -> Result { loop { let result = (self.f)(); match result { Ok(v) => return Ok(v), Err(err) => { if !(self.retryable)(&err) { return Err(err); } match self.backoff.next() { None => return Err(err), Some(dur) => { (self.notify)(&err, dur); self.sleep_fn.sleep(dur); } } } } } } } #[cfg(test)] mod tests { use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; use core::time::Duration; use spin::Mutex; use super::*; use crate::ExponentialBuilder; fn always_error() -> anyhow::Result<()> { Err(anyhow::anyhow!("test_query meets error")) } #[test] fn test_retry() -> anyhow::Result<()> { let result = always_error .retry(ExponentialBuilder::default().with_min_delay(Duration::from_millis(1))) .call(); assert!(result.is_err()); assert_eq!("test_query meets error", result.unwrap_err().to_string()); Ok(()) } #[test] fn test_retry_with_not_retryable_error() -> anyhow::Result<()> { let error_times = Mutex::new(0); let f = || { let mut x = error_times.lock(); *x += 1; Err::<(), anyhow::Error>(anyhow::anyhow!("not retryable")) }; let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let result = f .retry(backoff) // Only retry If error message is `retryable` .when(|e| e.to_string() == "retryable") .call(); assert!(result.is_err()); assert_eq!("not retryable", result.unwrap_err().to_string()); // `f` always returns error "not retryable", so it should be executed // only once. assert_eq!(*error_times.lock(), 1); Ok(()) } #[test] fn test_retry_with_retryable_error() -> anyhow::Result<()> { let error_times = Mutex::new(0); let f = || { // println!("I have been called!"); let mut x = error_times.lock(); *x += 1; Err::<(), anyhow::Error>(anyhow::anyhow!("retryable")) }; let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let result = f .retry(backoff) // Only retry If error message is `retryable` .when(|e| e.to_string() == "retryable") .call(); assert!(result.is_err()); assert_eq!("retryable", result.unwrap_err().to_string()); // `f` always returns error "retryable", so it should be executed // 4 times (retry 3 times). assert_eq!(*error_times.lock(), 4); Ok(()) } #[test] fn test_fn_mut_when_and_notify() -> anyhow::Result<()> { let mut calls_retryable: Vec<()> = vec![]; let mut calls_notify: Vec<()> = vec![]; let f = || Err::<(), anyhow::Error>(anyhow::anyhow!("retryable")); let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let result = f .retry(backoff) .when(|_| { calls_retryable.push(()); true }) .notify(|_, _| { calls_notify.push(()); }) .call(); assert!(result.is_err()); assert_eq!("retryable", result.unwrap_err().to_string()); // `f` always returns error "retryable", so it should be executed // 4 times (retry 3 times). assert_eq!(calls_retryable.len(), 4); assert_eq!(calls_notify.len(), 3); Ok(()) } } backon-1.2.0/src/blocking_retry_with_context.rs000064400000000000000000000145221046102023000200360ustar 00000000000000use core::time::Duration; use crate::backoff::BackoffBuilder; use crate::blocking_sleep::MaybeBlockingSleeper; use crate::{Backoff, BlockingSleeper, DefaultBlockingSleeper}; /// BlockingRetryableWithContext adds retry support for blocking functions. pub trait BlockingRetryableWithContext< B: BackoffBuilder, T, E, Ctx, F: FnMut(Ctx) -> (Ctx, Result), > { /// Generate a new retry fn retry(self, builder: B) -> BlockingRetryWithContext; } impl BlockingRetryableWithContext for F where B: BackoffBuilder, F: FnMut(Ctx) -> (Ctx, Result), { fn retry(self, builder: B) -> BlockingRetryWithContext { BlockingRetryWithContext::new(self, builder.build()) } } /// Retry structure generated by [`BlockingRetryableWithContext`]. pub struct BlockingRetryWithContext< B: Backoff, T, E, Ctx, F: FnMut(Ctx) -> (Ctx, Result), SF: MaybeBlockingSleeper = DefaultBlockingSleeper, RF = fn(&E) -> bool, NF = fn(&E, Duration), > { backoff: B, retryable: RF, notify: NF, f: F, sleep_fn: SF, ctx: Option, } impl BlockingRetryWithContext where B: Backoff, F: FnMut(Ctx) -> (Ctx, Result), { /// Create a new retry. fn new(f: F, backoff: B) -> Self { BlockingRetryWithContext { backoff, retryable: |_: &E| true, notify: |_: &E, _: Duration| {}, sleep_fn: DefaultBlockingSleeper::default(), f, ctx: None, } } } impl BlockingRetryWithContext where B: Backoff, F: FnMut(Ctx) -> (Ctx, Result), SF: MaybeBlockingSleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { /// Set the context for retrying. /// /// Context is used to capture ownership manually to prevent lifetime issues. pub fn context(self, context: Ctx) -> BlockingRetryWithContext { BlockingRetryWithContext { backoff: self.backoff, retryable: self.retryable, notify: self.notify, f: self.f, sleep_fn: self.sleep_fn, ctx: Some(context), } } /// Set the sleeper for retrying. /// /// The sleeper should implement the [`BlockingSleeper`] trait. The simplest way is to use a closure like `Fn(Duration)`. /// /// If not specified, we use the [`DefaultBlockingSleeper`]. pub fn sleep( self, sleep_fn: SN, ) -> BlockingRetryWithContext { BlockingRetryWithContext { backoff: self.backoff, retryable: self.retryable, notify: self.notify, f: self.f, sleep_fn, ctx: self.ctx, } } /// Set the conditions for retrying. /// /// If not specified, all errors are considered retryable. pub fn when bool>( self, retryable: RN, ) -> BlockingRetryWithContext { BlockingRetryWithContext { backoff: self.backoff, retryable, notify: self.notify, f: self.f, sleep_fn: self.sleep_fn, ctx: self.ctx, } } /// Set to notify for all retry attempts. /// /// When a retry happens, the input function will be invoked with the error and the sleep duration before pausing. /// /// If not specified, this operation does nothing. pub fn notify( self, notify: NN, ) -> BlockingRetryWithContext { BlockingRetryWithContext { backoff: self.backoff, retryable: self.retryable, notify, f: self.f, sleep_fn: self.sleep_fn, ctx: self.ctx, } } } impl BlockingRetryWithContext where B: Backoff, F: FnMut(Ctx) -> (Ctx, Result), SF: BlockingSleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { /// Call the retried function. /// /// TODO: implement [`FnOnce`] after it stable. pub fn call(mut self) -> (Ctx, Result) { let mut ctx = self.ctx.take().expect("context must be valid"); loop { let (xctx, result) = (self.f)(ctx); // return ctx ownership back ctx = xctx; match result { Ok(v) => return (ctx, Ok(v)), Err(err) => { if !(self.retryable)(&err) { return (ctx, Err(err)); } match self.backoff.next() { None => return (ctx, Err(err)), Some(dur) => { (self.notify)(&err, dur); self.sleep_fn.sleep(dur); } } } } } } } #[cfg(test)] mod tests { use super::*; use crate::ExponentialBuilder; use alloc::string::ToString; use anyhow::anyhow; use anyhow::Result; use core::time::Duration; use spin::Mutex; struct Test; impl Test { fn hello(&mut self) -> Result { Err(anyhow!("not retryable")) } } #[test] fn test_retry_with_not_retryable_error() -> Result<()> { let error_times = Mutex::new(0); let test = Test; let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let (_, result) = { |mut v: Test| { let mut x = error_times.lock(); *x += 1; let res = v.hello(); (v, res) } } .retry(backoff) .context(test) // Only retry If error message is `retryable` .when(|e| e.to_string() == "retryable") .call(); assert!(result.is_err()); assert_eq!("not retryable", result.unwrap_err().to_string()); // `f` always returns error "not retryable", so it should be executed // only once. assert_eq!(*error_times.lock(), 1); Ok(()) } } backon-1.2.0/src/blocking_sleep.rs000064400000000000000000000043421046102023000152010ustar 00000000000000use core::time::Duration; /// A sleeper is used sleep for a specified duration. pub trait BlockingSleeper: 'static { /// sleep for a specified duration. fn sleep(&self, dur: Duration); } /// A stub trait allowing non-[`BlockingSleeper`] types to be used as a generic parameter in [`BlockingRetry`][crate::BlockingRetry]. /// It does not provide actual functionality. #[doc(hidden)] pub trait MaybeBlockingSleeper: 'static {} /// All `BlockingSleeper` will implement `MaybeBlockingSleeper`, but not vice versa. impl MaybeBlockingSleeper for T {} /// All `Fn(Duration)` implements `Sleeper`. impl BlockingSleeper for F { fn sleep(&self, dur: Duration) { self(dur) } } /// The default implementation of `Sleeper` when no features are enabled. /// /// It will fail to compile if a containing [`Retry`][crate::Retry] is `.await`ed without calling [`Retry::sleep`][crate::Retry::sleep] to provide a valid sleeper. #[cfg(not(feature = "std-blocking-sleep"))] pub type DefaultBlockingSleeper = PleaseEnableAFeatureOrProvideACustomSleeper; /// The default implementation of `Sleeper` while feature `std-blocking-sleep` enabled. /// /// it uses [`std::thread::sleep`]. #[cfg(feature = "std-blocking-sleep")] pub type DefaultBlockingSleeper = StdSleeper; /// A placeholder type that does not implement [`Sleeper`] and will therefore fail to compile if used as one. /// /// Users should enable a feature of this crate that provides a valid [`Sleeper`] implementation when this type appears in compilation errors. Alternatively, a custom [`Sleeper`] implementation should be provided where necessary, such as in [`crate::Retry::sleeper`]. #[doc(hidden)] #[derive(Clone, Copy, Debug, Default)] pub struct PleaseEnableAFeatureOrProvideACustomSleeper; /// Implement `MaybeSleeper` but not `Sleeper`. impl MaybeBlockingSleeper for PleaseEnableAFeatureOrProvideACustomSleeper {} /// The implementation of `StdSleeper` uses [`std::thread::sleep`]. #[cfg(feature = "std-blocking-sleep")] #[derive(Clone, Copy, Debug, Default)] pub struct StdSleeper; #[cfg(feature = "std-blocking-sleep")] impl BlockingSleeper for StdSleeper { fn sleep(&self, dur: Duration) { std::thread::sleep(dur) } } backon-1.2.0/src/docs/examples/basic.md000064400000000000000000000005561046102023000160270ustar 00000000000000Retry an async function. ```rust use backon::ExponentialBuilder; use backon::Retryable; use anyhow::Result; async fn fetch() -> Result { Ok("Hello, World!".to_string()) } #[tokio::main] async fn main() -> Result<()> { let content = fetch.retry(ExponentialBuilder::default()).await?; println!("fetch succeeded: {}", content); Ok(()) } ``` backon-1.2.0/src/docs/examples/closure.md000064400000000000000000000005621046102023000164170ustar 00000000000000Retry an closure. ```rust use backon::ExponentialBuilder; use backon::Retryable; use backon::BlockingRetryable; fn main() -> anyhow::Result<()> { let var = 42; // `f` can use input variables let f = || Ok::(var); let result = f.retry(backon::ExponentialBuilder::default()).call()?; println!("var = {result}"); Ok(()) } ``` backon-1.2.0/src/docs/examples/inside_mut_self.md000064400000000000000000000011061046102023000201070ustar 00000000000000Retry an async function inside `&mut self` functions. ```rust use anyhow::Result; use backon::ExponentialBuilder; use backon::Retryable; struct Test; impl Test { async fn fetch(&self, url: &str) -> Result { Ok(reqwest::get(url).await?.text().await?) } async fn run(&mut self) -> Result { let content = (|| async { self.fetch("https://www.rust-lang.org").await }) .retry(ExponentialBuilder::default()) .when(|e| e.to_string() == "retryable") .await?; Ok(content) } } ``` backon-1.2.0/src/docs/examples/mod.rs000064400000000000000000000010201046102023000155340ustar 00000000000000//! Examples of using backon. #[doc = include_str!("basic.md")] pub mod basic {} #[doc = include_str!("closure.md")] pub mod closure {} #[doc = include_str!("inside_mut_self.md")] pub mod inside_mut_self {} #[doc = include_str!("sqlx.md")] pub mod sqlx {} #[doc = include_str!("with_args.md")] pub mod with_args {} #[doc = include_str!("with_mut_self.md")] pub mod with_mut_self {} #[doc = include_str!("with_self.md")] pub mod with_self {} #[doc = include_str!("with_specific_error.md")] pub mod with_specific_error {} backon-1.2.0/src/docs/examples/sqlx.md000064400000000000000000000007471046102023000157370ustar 00000000000000Retry sqlx operations. ```rust use backon::Retryable; use anyhow::Result; use backon::ExponentialBuilder; #[tokio::main] async fn main() -> Result<()> { let pool = sqlx::sqlite::SqlitePoolOptions::new() .max_connections(5) .connect("sqlite::memory:") .await?; let row: (i64,) = (|| sqlx::query_as("SELECT $1").bind(150_i64).fetch_one(&pool)) .retry(ExponentialBuilder::default()) .await?; assert_eq!(row.0, 150); Ok(()) } ``` backon-1.2.0/src/docs/examples/with_args.md000064400000000000000000000012601046102023000167260ustar 00000000000000Retry function with args. It's a pity that rust doesn't allow us to implement `Retryable` for async function with args. So we have to use a workaround to make it work. ```rust use anyhow::Result; use backon::ExponentialBuilder; use backon::Retryable; async fn fetch(url: &str) -> Result { Ok(reqwest::get(url).await?.text().await?) } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { let content = (|| async { fetch("https://www.rust-lang.org").await }) .retry(ExponentialBuilder::default()) .when(|e| e.to_string() == "retryable") .await?; println!("fetch succeeded: {}", content); Ok(()) } ``` backon-1.2.0/src/docs/examples/with_mut_self.md000064400000000000000000000017041046102023000176130ustar 00000000000000Retry an async function which takes `&mut self` as receiver. This is a bit more complex since we need to capture the receiver in the closure with ownership. backon supports this use case by `RetryableWithContext`. ```rust use anyhow::Result; use backon::ExponentialBuilder; use backon::RetryableWithContext; struct Test; impl Test { async fn fetch(&mut self, url: &str) -> Result { Ok(reqwest::get(url).await?.text().await?) } } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { let test = Test; let (_, result) = (|mut v: Test| async { let res = v.fetch("https://www.rust-lang.org").await; // Return input context back. (v, res) }) .retry(ExponentialBuilder::default()) // Passing context in. .context(test) .when(|e| e.to_string() == "retryable") .await; println!("fetch succeeded: {}", result.unwrap()); Ok(()) } ``` backon-1.2.0/src/docs/examples/with_self.md000064400000000000000000000012161046102023000167240ustar 00000000000000Retry an async function which takes `&self` as receiver. ```rust use anyhow::Result; use backon::ExponentialBuilder; use backon::Retryable; struct Test; impl Test { async fn fetch(&self, url: &str) -> Result { Ok(reqwest::get(url).await?.text().await?) } } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { let test = Test; let content = (|| async { test.fetch("https://www.rust-lang.org").await }) .retry(ExponentialBuilder::default()) .when(|e| e.to_string() == "retryable") .await?; println!("fetch succeeded: {}", content); Ok(()) } ``` backon-1.2.0/src/docs/examples/with_specific_error.md000064400000000000000000000007041046102023000207720ustar 00000000000000Retry with specify retryable error by `when`. ```rust use anyhow::Result; use backon::ExponentialBuilder; use backon::Retryable; async fn fetch() -> Result { Ok("Hello, World!".to_string()) } #[tokio::main] async fn main() -> Result<()> { let content = fetch .retry(ExponentialBuilder::default()) .when(|e| e.to_string() == "retryable") .await?; println!("fetch succeeded: {}", content); Ok(()) } ``` backon-1.2.0/src/docs/mod.rs000064400000000000000000000001051046102023000137210ustar 00000000000000//! Docs for the backon crate, like [`examples`]. pub mod examples; backon-1.2.0/src/lib.rs000064400000000000000000000126251046102023000127720ustar 00000000000000#![doc( html_logo_url = "https://raw.githubusercontent.com/Xuanwo/backon/main/.github/assets/logo.jpeg" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] //! [![Build Status]][actions] [![Latest Version]][crates.io] [![](https://img.shields.io/discord/1111711408875393035?logo=discord&label=discord)](https://discord.gg/8ARnvtJePD) //! //! [Build Status]: https://img.shields.io/github/actions/workflow/status/Xuanwo/backon/ci.yml?branch=main //! [actions]: https://github.com/Xuanwo/backon/actions?query=branch%3Amain //! [Latest Version]: https://img.shields.io/crates/v/backon.svg //! [crates.io]: https://crates.io/crates/backon //! //! BackON //! //! Make **retry** like a built-in feature provided by Rust. //! //! - **Simple**: Just like a built-in feature: `your_fn.retry(ExponentialBuilder::default()).await`. //! - **Flexible**: Supports both blocking and async functions. //! - **Powerful**: Allows control over retry behavior such as [`when`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.when) and [`notify`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.notify). //! - **Customizable**: Supports custom retry strategies like [exponential](https://docs.rs/backon/latest/backon/struct.ExponentialBuilder.html), [constant](https://docs.rs/backon/latest/backon/struct.ConstantBuilder.html), etc. //! //! # Backoff //! //! Retry in BackON requires a backoff strategy. BackON will accept a [`BackoffBuilder`] which will generate a new [`Backoff`] for each retry. //! //! BackON provides several backoff implementations with reasonable defaults: //! //! - [`ConstantBuilder`]: backoff with a constant delay, limited to a specific number of attempts. //! - [`ExponentialBuilder`]: backoff with an exponential delay, also supports jitter. //! - [`FibonacciBuilder`]: backoff with a fibonacci delay, also supports jitter. //! //! # Sleep //! //! Retry in BackON requires an implementation for sleeping. BackON will accept a [`Sleeper`] to pause for a specified duration. //! //! BackON employs the following default sleep implementations: //! //! - `tokio-sleep`: Utilizes [`TokioSleeper`] within a Tokio context in non-wasm32 environments. //! - `gloo-timers-sleep`: Utilizes [`GlooTimersSleep`] to pause in wasm32 environments. //! //! Users CAN provide a custom implementation if they prefer not to use the default options. //! //! If neither feature is enabled nor a custom implementation is provided, BackON will fallback to an empty sleeper. This will cause a panic in the `debug` profile and do nothing in the `release` profile. //! //! # Retry //! //! For additional examples, please visit [`docs::examples`]. //! //! ## Retry an async function //! //! ```rust //! use anyhow::Result; //! use backon::ExponentialBuilder; //! use backon::Retryable; //! use core::time::Duration; //! //! async fn fetch() -> Result { //! Ok("hello, world!".to_string()) //! } //! //! #[tokio::main] //! async fn main() -> Result<()> { //! let content = fetch //! // Retry with exponential backoff //! .retry(ExponentialBuilder::default()) //! // Sleep implementation, default to tokio::time::sleep if `tokio-sleep` has been enabled. //! .sleep(tokio::time::sleep) //! // When to retry //! .when(|e| e.to_string() == "EOF") //! // Notify when retrying //! .notify(|err: &anyhow::Error, dur: Duration| { //! println!("retrying {:?} after {:?}", err, dur); //! }) //! .await?; //! println!("fetch succeeded: {}", content); //! //! Ok(()) //! } //! ``` //! //! ## Retry a blocking function //! //! ```rust //! use anyhow::Result; //! use backon::BlockingRetryable; //! use backon::ExponentialBuilder; //! use core::time::Duration; //! //! fn fetch() -> Result { //! Ok("hello, world!".to_string()) //! } //! //! fn main() -> Result<()> { //! let content = fetch //! // Retry with exponential backoff //! .retry(ExponentialBuilder::default()) //! // When to retry //! .when(|e| e.to_string() == "EOF") //! // Notify when retrying //! .notify(|err: &anyhow::Error, dur: Duration| { //! println!("retrying {:?} after {:?}", err, dur); //! }) //! .call()?; //! println!("fetch succeeded: {}", content); //! //! Ok(()) //! } //! ``` #![deny(missing_docs)] #![deny(unused_qualifications)] #![no_std] #[cfg(feature = "std-blocking-sleep")] extern crate std; extern crate alloc; mod backoff; pub use backoff::*; mod retry; pub use retry::Retry; pub use retry::Retryable; mod retry_with_context; pub use retry_with_context::RetryWithContext; pub use retry_with_context::RetryableWithContext; mod sleep; pub use sleep::DefaultSleeper; #[cfg(all(target_arch = "wasm32", feature = "gloo-timers-sleep"))] pub use sleep::GlooTimersSleep; pub use sleep::Sleeper; #[cfg(all(not(target_arch = "wasm32"), feature = "tokio-sleep"))] pub use sleep::TokioSleeper; mod blocking_retry; pub use blocking_retry::{BlockingRetry, BlockingRetryable}; mod blocking_retry_with_context; pub use blocking_retry_with_context::{BlockingRetryWithContext, BlockingRetryableWithContext}; mod blocking_sleep; pub use blocking_sleep::BlockingSleeper; pub use blocking_sleep::DefaultBlockingSleeper; #[cfg(feature = "std-blocking-sleep")] pub use blocking_sleep::StdSleeper; #[cfg(docsrs)] pub mod docs; backon-1.2.0/src/retry.rs000064400000000000000000000330551046102023000133710ustar 00000000000000use core::future::Future; use core::pin::Pin; use core::task::ready; use core::task::Context; use core::task::Poll; use core::time::Duration; use crate::backoff::BackoffBuilder; use crate::sleep::MaybeSleeper; use crate::Backoff; use crate::DefaultSleeper; use crate::Sleeper; /// Retryable will add retry support for functions that produce futures with results. /// /// This means all types that implement `FnMut() -> impl Future>` /// will be able to use `retry`. /// /// For example: /// /// - Functions without extra args: /// /// ```ignore /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) /// } /// ``` /// /// - Closures /// /// ```ignore /// || async { /// let x = reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?; /// /// Err(anyhow::anyhow!(x)) /// } /// ``` pub trait Retryable< B: BackoffBuilder, T, E, Fut: Future>, FutureFn: FnMut() -> Fut, > { /// Generate a new retry fn retry(self, builder: B) -> Retry; } impl Retryable for FutureFn where B: BackoffBuilder, Fut: Future>, FutureFn: FnMut() -> Fut, { fn retry(self, builder: B) -> Retry { Retry::new(self, builder.build()) } } /// Struct generated by [`Retryable`]. pub struct Retry< B: Backoff, T, E, Fut: Future>, FutureFn: FnMut() -> Fut, SF: MaybeSleeper = DefaultSleeper, RF = fn(&E) -> bool, NF = fn(&E, Duration), > { backoff: B, retryable: RF, notify: NF, future_fn: FutureFn, sleep_fn: SF, state: State, } impl Retry where B: Backoff, Fut: Future>, FutureFn: FnMut() -> Fut, { /// Initiate a new retry. fn new(future_fn: FutureFn, backoff: B) -> Self { Retry { backoff, retryable: |_: &E| true, notify: |_: &E, _: Duration| {}, future_fn, sleep_fn: DefaultSleeper::default(), state: State::Idle, } } } impl Retry where B: Backoff, Fut: Future>, FutureFn: FnMut() -> Fut, SF: MaybeSleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { /// Set the sleeper for retrying. /// /// The sleeper should implement the [`Sleeper`] trait. The simplest way is to use a closure that returns a `Future`. /// /// If not specified, we use the [`DefaultSleeper`]. /// /// ```no_run /// use anyhow::Result; /// use backon::ExponentialBuilder; /// use backon::Retryable; /// use std::future::ready; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch /// .retry(ExponentialBuilder::default()) /// .sleep(|_| ready(())) /// .await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn sleep(self, sleep_fn: SN) -> Retry { Retry { backoff: self.backoff, retryable: self.retryable, notify: self.notify, future_fn: self.future_fn, sleep_fn, state: State::Idle, } } /// Set the conditions for retrying. /// /// If not specified, all errors are considered retryable. /// /// # Examples /// /// ```no_run /// use anyhow::Result; /// use backon::ExponentialBuilder; /// use backon::Retryable; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch /// .retry(ExponentialBuilder::default()) /// .when(|e| e.to_string() == "EOF") /// .await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn when bool>( self, retryable: RN, ) -> Retry { Retry { backoff: self.backoff, retryable, notify: self.notify, future_fn: self.future_fn, sleep_fn: self.sleep_fn, state: self.state, } } /// Set to notify for all retry attempts. /// /// When a retry happens, the input function will be invoked with the error and the sleep duration before pausing. /// /// If not specified, this operation does nothing. /// /// # Examples /// /// ```no_run /// use core::time::Duration; /// /// use anyhow::Result; /// use backon::ExponentialBuilder; /// use backon::Retryable; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch /// .retry(ExponentialBuilder::default()) /// .notify(|err: &anyhow::Error, dur: Duration| { /// println!("retrying error {:?} with sleeping {:?}", err, dur); /// }) /// .await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn notify( self, notify: NN, ) -> Retry { Retry { backoff: self.backoff, retryable: self.retryable, notify, sleep_fn: self.sleep_fn, future_fn: self.future_fn, state: self.state, } } } /// State maintains internal state of retry. #[derive(Default)] enum State>, SleepFut: Future> { #[default] Idle, Polling(Fut), Sleeping(SleepFut), } impl Future for Retry where B: Backoff, Fut: Future>, FutureFn: FnMut() -> Fut, SF: Sleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { // Safety: This is safe because we don't move the `Retry` struct itself, // only its internal state. // // We do the exactly same thing like `pin_project` but without depending on it directly. let this = unsafe { self.get_unchecked_mut() }; loop { match &mut this.state { State::Idle => { let fut = (this.future_fn)(); this.state = State::Polling(fut); continue; } State::Polling(fut) => { // Safety: This is safe because we don't move the `Retry` struct and this fut, // only its internal state. // // We do the exactly same thing like `pin_project` but without depending on it directly. let mut fut = unsafe { Pin::new_unchecked(fut) }; match ready!(fut.as_mut().poll(cx)) { Ok(v) => return Poll::Ready(Ok(v)), Err(err) => { // If input error is not retryable, return error directly. if !(this.retryable)(&err) { return Poll::Ready(Err(err)); } match this.backoff.next() { None => return Poll::Ready(Err(err)), Some(dur) => { (this.notify)(&err, dur); this.state = State::Sleeping(this.sleep_fn.sleep(dur)); continue; } } } } } State::Sleeping(sl) => { // Safety: This is safe because we don't move the `Retry` struct and this fut, // only its internal state. // // We do the exactly same thing like `pin_project` but without depending on it directly. let mut sl = unsafe { Pin::new_unchecked(sl) }; ready!(sl.as_mut().poll(cx)); this.state = State::Idle; continue; } } } } } #[cfg(test)] #[cfg(any(feature = "tokio-sleep", feature = "gloo-timers-sleep"))] mod default_sleeper_tests { use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; use core::time::Duration; use tokio::sync::Mutex; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test as test; #[cfg(not(target_arch = "wasm32"))] use tokio::test; use super::*; use crate::ExponentialBuilder; async fn always_error() -> anyhow::Result<()> { Err(anyhow::anyhow!("test_query meets error")) } #[test] async fn test_retry() -> anyhow::Result<()> { let result = always_error .retry(ExponentialBuilder::default().with_min_delay(Duration::from_millis(1))) .await; assert!(result.is_err()); assert_eq!("test_query meets error", result.unwrap_err().to_string()); Ok(()) } #[test] async fn test_retry_with_not_retryable_error() -> anyhow::Result<()> { let error_times = Mutex::new(0); let f = || async { let mut x = error_times.lock().await; *x += 1; Err::<(), anyhow::Error>(anyhow::anyhow!("not retryable")) }; let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let result = f .retry(backoff) // Only retry If error message is `retryable` .when(|e| e.to_string() == "retryable") .await; assert!(result.is_err()); assert_eq!("not retryable", result.unwrap_err().to_string()); // `f` always returns error "not retryable", so it should be executed // only once. assert_eq!(*error_times.lock().await, 1); Ok(()) } #[test] async fn test_retry_with_retryable_error() -> anyhow::Result<()> { let error_times = Mutex::new(0); let f = || async { let mut x = error_times.lock().await; *x += 1; Err::<(), anyhow::Error>(anyhow::anyhow!("retryable")) }; let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let result = f .retry(backoff) // Only retry If error message is `retryable` .when(|e| e.to_string() == "retryable") .await; assert!(result.is_err()); assert_eq!("retryable", result.unwrap_err().to_string()); // `f` always returns error "retryable", so it should be executed // 4 times (retry 3 times). assert_eq!(*error_times.lock().await, 4); Ok(()) } #[test] async fn test_fn_mut_when_and_notify() -> anyhow::Result<()> { let mut calls_retryable: Vec<()> = vec![]; let mut calls_notify: Vec<()> = vec![]; let f = || async { Err::<(), anyhow::Error>(anyhow::anyhow!("retryable")) }; let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let result = f .retry(backoff) .when(|_| { calls_retryable.push(()); true }) .notify(|_, _| { calls_notify.push(()); }) .await; assert!(result.is_err()); assert_eq!("retryable", result.unwrap_err().to_string()); // `f` always returns error "retryable", so it should be executed // 4 times (retry 3 times). assert_eq!(calls_retryable.len(), 4); assert_eq!(calls_notify.len(), 3); Ok(()) } } #[cfg(test)] mod custom_sleeper_tests { use alloc::string::ToString; use core::{future::ready, time::Duration}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test as test; #[cfg(not(target_arch = "wasm32"))] use tokio::test; use super::*; use crate::ExponentialBuilder; async fn always_error() -> anyhow::Result<()> { Err(anyhow::anyhow!("test_query meets error")) } #[test] async fn test_retry_with_sleep() -> anyhow::Result<()> { let result = always_error .retry(ExponentialBuilder::default().with_min_delay(Duration::from_millis(1))) .sleep(|_| ready(())) .await; assert!(result.is_err()); assert_eq!("test_query meets error", result.unwrap_err().to_string()); Ok(()) } } backon-1.2.0/src/retry_with_context.rs000064400000000000000000000314101046102023000161610ustar 00000000000000use core::future::Future; use core::pin::Pin; use core::task::ready; use core::task::Context; use core::task::Poll; use core::time::Duration; use crate::backoff::BackoffBuilder; use crate::sleep::MaybeSleeper; use crate::Backoff; use crate::DefaultSleeper; use crate::Sleeper; /// `RetryableWithContext` adds retry support for functions that produce futures with results /// and context. /// /// This means all types implementing `FnMut(Ctx) -> impl Future)>` /// can use `retry`. /// /// Users must provide context to the function and can receive it back after the retry is completed. /// /// # Example /// /// Without context, we might encounter errors such as the following: /// /// ```shell /// error: captured variable cannot escape `FnMut` closure body /// --> src/retry.rs:404:27 /// | /// 400 | let mut test = Test; /// | -------- variable defined here /// ... /// 404 | let result = { || async { test.hello().await } } /// | - ^^^^^^^^----^^^^^^^^^^^^^^^^ /// | | | | /// | | | variable captured here /// | | returns an `async` block that contains a reference to a captured variable, which then escapes the closure body /// | inferred to be a `FnMut` closure /// | /// = note: `FnMut` closures only have access to their captured variables while they are executing... /// = note: ...therefore, they cannot allow references to captured variables to escape /// ``` /// /// However, with context support, we can implement it this way: /// /// ```no_run /// use anyhow::anyhow; /// use anyhow::Result; /// use backon::ExponentialBuilder; /// use backon::RetryableWithContext; /// /// struct Test; /// /// impl Test { /// async fn hello(&mut self) -> Result { /// Err(anyhow!("not retryable")) /// } /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let mut test = Test; /// /// // (Test, Result) /// let (_, result) = { /// |mut v: Test| async { /// let res = v.hello().await; /// (v, res) /// } /// } /// .retry(ExponentialBuilder::default()) /// .context(test) /// .await; /// /// Ok(()) /// } /// ``` pub trait RetryableWithContext< B: BackoffBuilder, T, E, Ctx, Fut: Future)>, FutureFn: FnMut(Ctx) -> Fut, > { /// Generate a new retry fn retry(self, builder: B) -> RetryWithContext; } impl RetryableWithContext for FutureFn where B: BackoffBuilder, Fut: Future)>, FutureFn: FnMut(Ctx) -> Fut, { fn retry(self, builder: B) -> RetryWithContext { RetryWithContext::new(self, builder.build()) } } /// Retry struct generated by [`RetryableWithContext`]. pub struct RetryWithContext< B: Backoff, T, E, Ctx, Fut: Future)>, FutureFn: FnMut(Ctx) -> Fut, SF: MaybeSleeper = DefaultSleeper, RF = fn(&E) -> bool, NF = fn(&E, Duration), > { backoff: B, retryable: RF, notify: NF, future_fn: FutureFn, sleep_fn: SF, state: State, } impl RetryWithContext where B: Backoff, Fut: Future)>, FutureFn: FnMut(Ctx) -> Fut, { /// Create a new retry. fn new(future_fn: FutureFn, backoff: B) -> Self { RetryWithContext { backoff, retryable: |_: &E| true, notify: |_: &E, _: Duration| {}, future_fn, sleep_fn: DefaultSleeper::default(), state: State::Idle(None), } } } impl RetryWithContext where B: Backoff, Fut: Future)>, FutureFn: FnMut(Ctx) -> Fut, SF: Sleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { /// Set the sleeper for retrying. /// /// The sleeper should implement the [`Sleeper`] trait. The simplest way is to use a closure that returns a `Future`. /// /// If not specified, we use the [`DefaultSleeper`]. pub fn sleep( self, sleep_fn: SN, ) -> RetryWithContext { assert!( matches!(self.state, State::Idle(None)), "sleep must be set before context" ); RetryWithContext { backoff: self.backoff, retryable: self.retryable, notify: self.notify, future_fn: self.future_fn, sleep_fn, state: State::Idle(None), } } /// Set the context for retrying. /// /// Context is used to capture ownership manually to prevent lifetime issues. pub fn context( self, context: Ctx, ) -> RetryWithContext { RetryWithContext { backoff: self.backoff, retryable: self.retryable, notify: self.notify, future_fn: self.future_fn, sleep_fn: self.sleep_fn, state: State::Idle(Some(context)), } } /// Set the conditions for retrying. /// /// If not specified, all errors are considered retryable. /// /// # Examples /// /// ```no_run /// use anyhow::Result; /// use backon::ExponentialBuilder; /// use backon::Retryable; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch /// .retry(ExponentialBuilder::default()) /// .when(|e| e.to_string() == "EOF") /// .await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn when bool>( self, retryable: RN, ) -> RetryWithContext { RetryWithContext { backoff: self.backoff, retryable, notify: self.notify, future_fn: self.future_fn, sleep_fn: self.sleep_fn, state: self.state, } } /// Set to notify for all retry attempts. /// /// When a retry happens, the input function will be invoked with the error and the sleep duration before pausing. /// /// If not specified, this operation does nothing. /// /// # Examples /// /// ```no_run /// use core::time::Duration; /// /// use anyhow::Result; /// use backon::ExponentialBuilder; /// use backon::Retryable; /// /// async fn fetch() -> Result { /// Ok(reqwest::get("https://www.rust-lang.org") /// .await? /// .text() /// .await?) /// } /// /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<()> { /// let content = fetch /// .retry(ExponentialBuilder::default()) /// .notify(|err: &anyhow::Error, dur: Duration| { /// println!("retrying error {:?} with sleeping {:?}", err, dur); /// }) /// .await?; /// println!("fetch succeeded: {}", content); /// /// Ok(()) /// } /// ``` pub fn notify( self, notify: NN, ) -> RetryWithContext { RetryWithContext { backoff: self.backoff, retryable: self.retryable, notify, future_fn: self.future_fn, sleep_fn: self.sleep_fn, state: self.state, } } } /// State maintains internal state of retry. enum State)>, SleepFut: Future> { Idle(Option), Polling(Fut), Sleeping((Option, SleepFut)), } impl Future for RetryWithContext where B: Backoff, Fut: Future)>, FutureFn: FnMut(Ctx) -> Fut, SF: Sleeper, RF: FnMut(&E) -> bool, NF: FnMut(&E, Duration), { type Output = (Ctx, Result); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { // Safety: This is safe because we don't move the `Retry` struct itself, // only its internal state. // // We do the exactly same thing like `pin_project` but without depending on it directly. let this = unsafe { self.get_unchecked_mut() }; loop { match &mut this.state { State::Idle(ctx) => { let ctx = ctx.take().expect("context must be valid"); let fut = (this.future_fn)(ctx); this.state = State::Polling(fut); continue; } State::Polling(fut) => { // Safety: This is safe because we don't move the `Retry` struct and this fut, // only its internal state. // // We do the exactly same thing like `pin_project` but without depending on it directly. let mut fut = unsafe { Pin::new_unchecked(fut) }; let (ctx, res) = ready!(fut.as_mut().poll(cx)); match res { Ok(v) => return Poll::Ready((ctx, Ok(v))), Err(err) => { // If input error is not retryable, return error directly. if !(this.retryable)(&err) { return Poll::Ready((ctx, Err(err))); } match this.backoff.next() { None => return Poll::Ready((ctx, Err(err))), Some(dur) => { (this.notify)(&err, dur); this.state = State::Sleeping((Some(ctx), this.sleep_fn.sleep(dur))); continue; } } } } } State::Sleeping((ctx, sl)) => { // Safety: This is safe because we don't move the `Retry` struct and this fut, // only its internal state. // // We do the exactly same thing like `pin_project` but without depending on it directly. let mut sl = unsafe { Pin::new_unchecked(sl) }; ready!(sl.as_mut().poll(cx)); let ctx = ctx.take().expect("context must be valid"); this.state = State::Idle(Some(ctx)); continue; } } } } } #[cfg(test)] #[cfg(any(feature = "tokio-sleep", feature = "gloo-timers-sleep"))] mod tests { use alloc::string::ToString; use anyhow::{anyhow, Result}; use core::time::Duration; use tokio::sync::Mutex; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test as test; #[cfg(not(target_arch = "wasm32"))] use tokio::test; use super::*; use crate::ExponentialBuilder; struct Test; impl Test { async fn hello(&mut self) -> Result { Err(anyhow!("not retryable")) } } #[test] async fn test_retry_with_not_retryable_error() -> Result<()> { let error_times = Mutex::new(0); let test = Test; let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)); let (_, result) = { |mut v: Test| async { let mut x = error_times.lock().await; *x += 1; let res = v.hello().await; (v, res) } } .retry(backoff) .context(test) // Only retry If error message is `retryable` .when(|e| e.to_string() == "retryable") .await; assert!(result.is_err()); assert_eq!("not retryable", result.unwrap_err().to_string()); // `f` always returns error "not retryable", so it should be executed // only once. assert_eq!(*error_times.lock().await, 1); Ok(()) } } backon-1.2.0/src/sleep.rs000064400000000000000000000070171046102023000133330ustar 00000000000000use core::{ future::{Future, Ready}, time::Duration, }; /// A sleeper is used to generate a future that completes after a specified duration. pub trait Sleeper: 'static { /// The future returned by the `sleep` method. type Sleep: Future; /// Create a future that completes after a set period. fn sleep(&self, dur: Duration) -> Self::Sleep; } /// A stub trait allowing non-[`Sleeper`] types to be used as a generic parameter in [`Retry`][crate::Retry]. /// It does not provide actual functionality. #[doc(hidden)] pub trait MaybeSleeper: 'static { type Sleep: Future; } /// All `Sleeper` will implement `MaybeSleeper`, but not vice versa. impl MaybeSleeper for T { type Sleep = ::Sleep; } /// All `Fn(Duration) -> impl Future` implements `Sleeper`. impl Fut + 'static, Fut: Future> Sleeper for F { type Sleep = Fut; fn sleep(&self, dur: Duration) -> Self::Sleep { self(dur) } } /// The default implementation of `Sleeper` when no features are enabled. /// /// It will fail to compile if a containing [`Retry`][crate::Retry] is `.await`ed without calling [`Retry::sleep`][crate::Retry::sleep] to provide a valid sleeper. #[cfg(all(not(feature = "tokio-sleep"), not(feature = "gloo-timers-sleep")))] pub type DefaultSleeper = PleaseEnableAFeatureOrProvideACustomSleeper; /// The default implementation of `Sleeper` while feature `tokio-sleep` enabled. /// /// it uses `tokio::time::sleep`. #[cfg(all(not(target_arch = "wasm32"), feature = "tokio-sleep"))] pub type DefaultSleeper = TokioSleeper; /// The default implementation of `Sleeper` while feature `gloo-timers-sleep` enabled. /// /// It uses `gloo_timers::sleep::sleep`. #[cfg(all(target_arch = "wasm32", feature = "gloo-timers-sleep"))] pub type DefaultSleeper = GlooTimersSleep; /// A placeholder type that does not implement [`Sleeper`] and will therefore fail to compile if used as one. /// /// Users should enable a feature of this crate that provides a valid [`Sleeper`] implementation when this type appears in compilation errors. Alternatively, a custom [`Sleeper`] implementation should be provided where necessary, such as in [`crate::Retry::sleeper`]. #[doc(hidden)] #[derive(Clone, Copy, Debug, Default)] pub struct PleaseEnableAFeatureOrProvideACustomSleeper; /// Implement `MaybeSleeper` but not `Sleeper`. impl MaybeSleeper for PleaseEnableAFeatureOrProvideACustomSleeper { type Sleep = Ready<()>; } /// The default implementation of `Sleeper` uses `tokio::time::sleep`. /// /// It will adhere to [pausing/auto-advancing](https://docs.rs/tokio/latest/tokio/time/fn.pause.html) /// in Tokio's Runtime semantics, if enabled. #[cfg(all(not(target_arch = "wasm32"), feature = "tokio-sleep"))] #[derive(Clone, Copy, Debug, Default)] pub struct TokioSleeper; #[cfg(all(not(target_arch = "wasm32"), feature = "tokio-sleep"))] impl Sleeper for TokioSleeper { type Sleep = tokio::time::Sleep; fn sleep(&self, dur: Duration) -> Self::Sleep { tokio::time::sleep(dur) } } /// The default implementation of `Sleeper` utilizes `gloo_timers::future::sleep`. #[cfg(all(target_arch = "wasm32", feature = "gloo-timers-sleep"))] #[derive(Clone, Copy, Debug, Default)] pub struct GlooTimersSleep; #[cfg(all(target_arch = "wasm32", feature = "gloo-timers-sleep"))] impl Sleeper for GlooTimersSleep { type Sleep = gloo_timers::future::TimeoutFuture; fn sleep(&self, dur: Duration) -> Self::Sleep { gloo_timers::future::sleep(dur) } }