From 2b22833cc5a317eb354f60b497ece24edce0da3f Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Thu, 14 Apr 2022 12:54:25 +0800 Subject: [PATCH 1/5] Save work Signed-off-by: Xuanwo --- Cargo.toml | 4 +- src/exponential.rs | 27 ++++--- src/lib.rs | 4 ++ src/policy.rs | 4 +- src/retry.rs | 171 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 src/retry.rs diff --git a/Cargo.toml b/Cargo.toml index cb34fbb..c1e626f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,10 @@ description = "Backoff policies" [dependencies] rand = "0.8.5" +futures = "0.3.21" +tokio = { version = "1.17.0", features = ["full"] } +pin-project = "1.0.10" [dev-dependencies] -tokio = { version = "1.17.0", features = ["full"] } reqwest = "0.11.10" anyhow = "1.0.56" diff --git a/src/exponential.rs b/src/exponential.rs index 2ede9b7..469ee34 100644 --- a/src/exponential.rs +++ b/src/exponential.rs @@ -5,30 +5,27 @@ use std::time::Duration; /// /// # Examples /// -/// ``` +/// ```no_run /// use backon::ExponentialBackoff; +/// use backon::retry; /// use anyhow::Result; /// /// #[tokio::main] /// async fn main() -> Result<()> { -/// for delay in ExponentialBackoff::default() { -/// let x = reqwest::get("https://www.rust-lang.org").await?.text().await; -/// match x { -/// Ok(v) => { -/// println!("Successfully fetched"); -/// break; -/// }, -/// Err(_) => { -/// tokio::time::sleep(delay).await; -/// continue -/// } -/// }; -/// } +/// let v = retry( +/// ExponentialBackoff::default(), +/// |dur| tokio::time::sleep(dur), +/// |_: &anyhow::Error| true, +/// || async { +/// Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) +/// } +/// ).await?; /// +/// println!("Fetch https://www.rust-lang.org successfully!"); /// Ok(()) /// } /// ``` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ExponentialBackoff { jitter: bool, factor: f32, diff --git a/src/lib.rs b/src/lib.rs index 04de5ee..5cefeae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,5 +2,9 @@ mod constant; pub use constant::ConstantBackoff; mod exponential; pub use exponential::ExponentialBackoff; + mod policy; pub use policy::Policy; + +mod retry; +pub use retry::retry; diff --git a/src/policy.rs b/src/policy.rs index 856354c..6fb731f 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -1,4 +1,4 @@ use std::time::Duration; -pub trait Policy: Iterator {} -impl Policy for T where T: Iterator {} +pub trait Policy: Iterator + Clone {} +impl Policy for T where T: Iterator + Clone {} diff --git a/src/retry.rs b/src/retry.rs new file mode 100644 index 0000000..ee8a686 --- /dev/null +++ b/src/retry.rs @@ -0,0 +1,171 @@ +use crate::{ExponentialBackoff, Policy}; +use futures::ready; +use pin_project::pin_project; +use std::env::Args; +use std::future::Future; +use std::mem; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +trait Retryable>, FutureFn: FnMut() -> Fut> { + fn retry(self) -> Retry; +} + +impl Retryable for FutureFn +where + Fut: Future>, + FutureFn: FnMut() -> Fut, +{ + fn retry(self) -> Retry { + Retry { + backoff: ExponentialBackoff::default(), + error_fn: |_: &E| true, + future_fn: self, + state: State::Idle, + } + } +} + +#[pin_project] +struct Retry>, FutureFn: FnMut() -> Fut> { + backoff: ExponentialBackoff, + error_fn: fn(&E) -> bool, + future_fn: FutureFn, + + #[pin] + state: State, +} + +#[pin_project(project = StateProject)] +enum State>> { + Idle, + + Polling(#[pin] Fut), + // TODO: we need to support other sleeper + Sleeping(#[pin] tokio::time::Sleep), +} + +impl Default for State +where + Fut: Future>, +{ + fn default() -> Self { + State::Idle + } +} + +impl Future for Retry +where + Fut: Future>, + FutureFn: FnMut() -> Fut, +{ + type Output = std::result::Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + loop { + let state = this.state.as_mut().project(); + match state { + StateProject::Idle => { + let fut = (this.future_fn)(); + // this.state = State::Polling(fut); + this.state.set(State::Polling(fut)); + continue; + } + StateProject::Polling(fut) => match ready!(fut.poll(cx)) { + Ok(v) => return Poll::Ready(Ok(v)), + Err(err) => match this.backoff.next() { + None => return Poll::Ready(Err(err)), + Some(dur) => { + this.state.set(State::Sleeping(tokio::time::sleep(dur))); + continue; + } + }, + }, + StateProject::Sleeping(sl) => { + ready!(sl.poll(cx)); + this.state.set(State::Idle); + continue; + } + } + } + } +} + +pub async fn retry( + mut backoff: B, + mut sleep_fn: FS, + mut error_fn: FE, + mut future_fn: FF, +) -> std::result::Result +where + B: Policy, + FF: FnMut() -> FUT, + FE: FnMut(&E) -> bool, + FS: FnMut(Duration) -> FSF, + FUT: Future>, + FSF: Future, +{ + loop { + match (future_fn)().await { + Ok(v) => return Ok(v), + Err(err) => { + if !(error_fn)(&err) { + return Err(err); + } + + match backoff.next() { + None => return Err(err), + Some(dur) => { + (sleep_fn)(dur).await; + continue; + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_retry() -> anyhow::Result<()> { + let x = { + || async { + let x = reqwest::get("https://www.rust-lang.org") + .await? + .text() + .await?; + + Err(anyhow::anyhow!(x)) + } + } + .retry() + .await?; + + println!("got: {:?}", x); + + Ok(()) + } + + async fn test_query() -> anyhow::Result<()> { + let x = reqwest::get("https://www.rust-lang.org") + .await? + .text() + .await?; + + Err(anyhow::anyhow!(x)) + } + + #[tokio::test] + async fn test_retry_x() -> anyhow::Result<()> { + let x = test_query.retry().await?; + + println!("got: {:?}", x); + + Ok(()) + } +} From eac0732ce22a121bccedffd0b26fdaea6a7f4cc0 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Thu, 14 Apr 2022 13:05:00 +0800 Subject: [PATCH 2/5] Oh, it works Signed-off-by: Xuanwo --- src/retry.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/retry.rs b/src/retry.rs index ee8a686..74b4618 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -8,18 +8,26 @@ use std::pin::Pin; use std::task::{Context, Poll}; use std::time::Duration; -trait Retryable>, FutureFn: FnMut() -> Fut> { - fn retry(self) -> Retry; +trait Retryable< + P: Policy, + T, + E, + Fut: Future>, + FutureFn: FnMut() -> Fut, +> +{ + fn retry(self, policy: P) -> Retry; } -impl Retryable for FutureFn +impl Retryable for FutureFn where + P: Policy, Fut: Future>, FutureFn: FnMut() -> Fut, { - fn retry(self) -> Retry { + fn retry(self, policy: P) -> Retry { Retry { - backoff: ExponentialBackoff::default(), + backoff: policy, error_fn: |_: &E| true, future_fn: self, state: State::Idle, @@ -28,8 +36,14 @@ where } #[pin_project] -struct Retry>, FutureFn: FnMut() -> Fut> { - backoff: ExponentialBackoff, +struct Retry< + P: Policy, + T, + E, + Fut: Future>, + FutureFn: FnMut() -> Fut, +> { + backoff: P, error_fn: fn(&E) -> bool, future_fn: FutureFn, @@ -55,8 +69,9 @@ where } } -impl Future for Retry +impl Future for Retry where + P: Policy, Fut: Future>, FutureFn: FnMut() -> Fut, { @@ -143,7 +158,7 @@ mod tests { Err(anyhow::anyhow!(x)) } } - .retry() + .retry(ExponentialBackoff::default()) .await?; println!("got: {:?}", x); @@ -162,7 +177,7 @@ mod tests { #[tokio::test] async fn test_retry_x() -> anyhow::Result<()> { - let x = test_query.retry().await?; + let x = test_query.retry(ExponentialBackoff::default()).await?; println!("got: {:?}", x); From 61240d1ec90da5c4b43709e7b61864e3f055f5e1 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Thu, 14 Apr 2022 13:25:17 +0800 Subject: [PATCH 3/5] Fix all warnings Signed-off-by: Xuanwo --- src/lib.rs | 4 ++-- src/policy.rs | 4 ++-- src/retry.rs | 61 ++++++++++++--------------------------------------- 3 files changed, 18 insertions(+), 51 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5cefeae..de27b2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod exponential; pub use exponential::ExponentialBackoff; mod policy; -pub use policy::Policy; +pub use policy::Backoff; mod retry; -pub use retry::retry; +pub use retry::Retryable; diff --git a/src/policy.rs b/src/policy.rs index 6fb731f..654d184 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -1,4 +1,4 @@ use std::time::Duration; -pub trait Policy: Iterator + Clone {} -impl Policy for T where T: Iterator + Clone {} +pub trait Backoff: Iterator + Clone {} +impl Backoff for T where T: Iterator + Clone {} diff --git a/src/retry.rs b/src/retry.rs index 74b4618..029ab77 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -1,15 +1,14 @@ -use crate::{ExponentialBackoff, Policy}; +use crate::Backoff; use futures::ready; use pin_project::pin_project; -use std::env::Args; + use std::future::Future; -use std::mem; + use std::pin::Pin; use std::task::{Context, Poll}; -use std::time::Duration; -trait Retryable< - P: Policy, +pub trait Retryable< + P: Backoff, T, E, Fut: Future>, @@ -21,7 +20,7 @@ trait Retryable< impl Retryable for FutureFn where - P: Policy, + P: Backoff, Fut: Future>, FutureFn: FnMut() -> Fut, { @@ -36,8 +35,8 @@ where } #[pin_project] -struct Retry< - P: Policy, +pub struct Retry< + P: Backoff, T, E, Fut: Future>, @@ -57,7 +56,7 @@ enum State>> { Polling(#[pin] Fut), // TODO: we need to support other sleeper - Sleeping(#[pin] tokio::time::Sleep), + Sleeping(#[pin] Pin>), } impl Default for State @@ -71,13 +70,13 @@ where impl Future for Retry where - P: Policy, + P: Backoff, Fut: Future>, FutureFn: FnMut() -> Fut, { type Output = std::result::Result; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut this = self.project(); loop { let state = this.state.as_mut().project(); @@ -93,7 +92,8 @@ where Err(err) => match this.backoff.next() { None => return Poll::Ready(Err(err)), Some(dur) => { - this.state.set(State::Sleeping(tokio::time::sleep(dur))); + this.state + .set(State::Sleeping(Box::pin(tokio::time::sleep(dur)))); continue; } }, @@ -108,43 +108,10 @@ where } } -pub async fn retry( - mut backoff: B, - mut sleep_fn: FS, - mut error_fn: FE, - mut future_fn: FF, -) -> std::result::Result -where - B: Policy, - FF: FnMut() -> FUT, - FE: FnMut(&E) -> bool, - FS: FnMut(Duration) -> FSF, - FUT: Future>, - FSF: Future, -{ - loop { - match (future_fn)().await { - Ok(v) => return Ok(v), - Err(err) => { - if !(error_fn)(&err) { - return Err(err); - } - - match backoff.next() { - None => return Err(err), - Some(dur) => { - (sleep_fn)(dur).await; - continue; - } - } - } - } - } -} - #[cfg(test)] mod tests { use super::*; + use crate::exponential::ExponentialBackoff; #[tokio::test] async fn test_retry() -> anyhow::Result<()> { From cb6481b4930fb7b5de38ae0807a3b125b0ce4680 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Thu, 14 Apr 2022 13:28:54 +0800 Subject: [PATCH 4/5] Rename policy to backoff Signed-off-by: Xuanwo --- Cargo.toml | 3 ++- src/{policy.rs => backoff.rs} | 0 src/lib.rs | 7 ++++--- src/retry.rs | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) rename src/{policy.rs => backoff.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index c1e626f..caf1f32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,10 @@ description = "Backoff policies" [dependencies] rand = "0.8.5" futures = "0.3.21" -tokio = { version = "1.17.0", features = ["full"] } +tokio = { version = "1.17.0", features = ["time"] } pin-project = "1.0.10" [dev-dependencies] +tokio = { version = "1.17.0", features = ["full"] } reqwest = "0.11.10" anyhow = "1.0.56" diff --git a/src/policy.rs b/src/backoff.rs similarity index 100% rename from src/policy.rs rename to src/backoff.rs diff --git a/src/lib.rs b/src/lib.rs index de27b2a..22e79de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ +mod backoff; +pub use backoff::Backoff; + mod constant; pub use constant::ConstantBackoff; + mod exponential; pub use exponential::ExponentialBackoff; -mod policy; -pub use policy::Backoff; - mod retry; pub use retry::Retryable; diff --git a/src/retry.rs b/src/retry.rs index 029ab77..048a6aa 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -1,12 +1,12 @@ -use crate::Backoff; use futures::ready; use pin_project::pin_project; use std::future::Future; - use std::pin::Pin; use std::task::{Context, Poll}; +use crate::Backoff; + pub trait Retryable< P: Backoff, T, From abe5f9f050210db18fff849cf592e408039c1c30 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Thu, 14 Apr 2022 14:04:27 +0800 Subject: [PATCH 5/5] Implement retry support Signed-off-by: Xuanwo --- README.md | 22 +++++------- src/backoff.rs | 4 +++ src/constant.rs | 28 +++++++++++++++ src/exponential.rs | 41 ++++++++++++++++------ src/lib.rs | 35 +++++++++++++++++++ src/retry.rs | 86 ++++++++++++++++++++++++++++++---------------- 6 files changed, 162 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 9247b11..12faf29 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,23 @@ The opposite backoff implementation of the popular [backoff](https://docs.rs/bac - Newer: developed by Rust edition 2021 and latest stable. - Cleaner: Iterator based abstraction, easy to use, customization friendly. -- Smaller: Focused on backoff implementation, no need to play with runtime specific features. +- Easier: Trait based implementations, works like a native function provided by closures. ## Quick Start ```rust +use backon::Retryable; use backon::ExponentialBackoff; use anyhow::Result; +async fn fetch() -> Result { + Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) +} + #[tokio::main] async fn main() -> Result<()> { - for delay in ExponentialBackoff::default() { - let x = reqwest::get("https://www.rust-lang.org").await?.text().await; - match x { - Ok(v) => { - println!("Successfully fetched"); - break; - }, - Err(_) => { - tokio::time::sleep(delay).await; - continue - } - }; - } + let content = fetch.retry(ExponentialBackoff::default()).await?; + println!("fetch succeeded: {}", contet); Ok(()) } diff --git a/src/backoff.rs b/src/backoff.rs index 654d184..353f453 100644 --- a/src/backoff.rs +++ b/src/backoff.rs @@ -1,4 +1,8 @@ use std::time::Duration; +/// Backoff is an [`Iterator`] that returns [`Duration`]. +/// +/// - `Some(Duration)` means caller need to `sleep(Duration)` and retry the same request +/// - `None` means we have reaching the limits, caller needs to return current error instead. pub trait Backoff: Iterator + Clone {} impl Backoff for T where T: Iterator + Clone {} diff --git a/src/constant.rs b/src/constant.rs index e92cf0a..9016888 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -1,5 +1,31 @@ use std::time::Duration; +/// ConstantBackoff provides backoff with constant delay and limited times. +/// +/// # Default +/// +/// - delay: 1s +/// - max times: 3 +/// +/// # Examples +/// +/// ```no_run +/// use backon::Retryable; +/// use backon::ConstantBackoff; +/// use anyhow::Result; +/// +/// async fn fetch() -> Result { +/// Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) +/// } +/// +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let content = fetch.retry(ConstantBackoff::default()).await?; +/// println!("fetch succeeded: {}", content); +/// +/// Ok(()) +/// } +/// ``` pub struct ConstantBackoff { delay: Duration, max_times: Option, @@ -18,11 +44,13 @@ impl Default for ConstantBackoff { } impl ConstantBackoff { + /// Set delay of current backoff. pub fn with_delay(mut self, delay: Duration) -> Self { self.delay = delay; self } + /// Set max times of current backoff. pub fn with_max_times(mut self, max_times: usize) -> Self { self.max_times = Some(max_times); self diff --git a/src/exponential.rs b/src/exponential.rs index 469ee34..7dd47a7 100644 --- a/src/exponential.rs +++ b/src/exponential.rs @@ -3,25 +3,30 @@ use std::time::Duration; /// Exponential backoff implementation. /// +/// # Default +/// +/// - jitter: false +/// - factor: 2 +/// - min_delay: 1s +/// - max_delay: 60s +/// - max_times: 3 +/// /// # Examples /// /// ```no_run +/// use backon::Retryable; /// use backon::ExponentialBackoff; -/// use backon::retry; /// use anyhow::Result; /// +/// async fn fetch() -> Result { +/// Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) +/// } +/// /// #[tokio::main] /// async fn main() -> Result<()> { -/// let v = retry( -/// ExponentialBackoff::default(), -/// |dur| tokio::time::sleep(dur), -/// |_: &anyhow::Error| true, -/// || async { -/// Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) -/// } -/// ).await?; +/// let content = fetch.retry(ExponentialBackoff::default()).await?; +/// println!("fetch succeeded: {}", content); /// -/// println!("Fetch https://www.rust-lang.org successfully!"); /// Ok(()) /// } /// ``` @@ -53,11 +58,20 @@ impl Default for ExponentialBackoff { } impl ExponentialBackoff { + /// Set jitter of current backoff. + /// + /// If jitter is enabled, ExponentialBackoff will add a random jitter in `[0, min_delay) + /// to current delay. pub fn with_jitter(mut self) -> Self { self.jitter = true; self } + /// Set factor of current backoff. + /// + /// # Panics + /// + /// This function will panic if input factor smaller than `1.0`. pub fn with_factor(mut self, factor: f32) -> Self { debug_assert!(factor > 1.0, "invalid factor that lower than 1"); @@ -65,16 +79,23 @@ impl ExponentialBackoff { self } + /// Set min_delay of current backoff. pub fn with_min_delay(mut self, min_delay: Duration) -> Self { self.min_delay = min_delay; self } + /// Set max_delay of current backoff. + /// + /// Delay will not increasing if current delay is larger than max_delay. pub fn with_max_delay(mut self, max_delay: Duration) -> Self { self.max_delay = Some(max_delay); self } + /// Set max_times of current backoff. + /// + /// Backoff will return `None` if max times is reaching. pub fn with_max_times(mut self, max_times: usize) -> Self { self.max_times = Some(max_times); self diff --git a/src/lib.rs b/src/lib.rs index 22e79de..07b8103 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,38 @@ +//! backon intends to provide an opposite backoff implementation of the popular [backoff](https://docs.rs/backoff). +//! +//! - Newer: developed by Rust edition 2021 and latest stable. +//! - Cleaner: Iterator based abstraction, easy to use, customization friendly. +//! - Easier: Trait based implementations, works like a native function provided by closures. +//! +//! # Backoff +//! +//! Any types that implements `Iterator` can be used as backoff. +//! +//! backon also provides backoff implementations with reasonable defaults: +//! +//! - [`ConstantBackoff`]: backoff with constant delay and limited times. +//! - [`ExponentialBackoff`]: backoff with exponential delay, also provides jitter supports. +//! +//! # Examples +//! +//! ```no_run +//! use backon::Retryable; +//! use backon::ExponentialBackoff; +//! use anyhow::Result; +//! +//! async fn fetch() -> Result { +//! Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) +//! } +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let content = fetch.retry(ExponentialBackoff::default()).await?; +//! println!("fetch succeeded: {}", content); +//! +//! Ok(()) +//! } +//! ``` + mod backoff; pub use backoff::Backoff; diff --git a/src/retry.rs b/src/retry.rs index 048a6aa..802cb53 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -7,6 +7,53 @@ use std::task::{Context, Poll}; use crate::Backoff; +/// Retryable will add retry support for functions that produces a futures with results. +/// +/// That 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)) +/// } +/// ``` +/// +/// # Example +/// +/// ```no_run +/// use backon::Retryable; +/// use backon::ExponentialBackoff; +/// use anyhow::Result; +/// +/// async fn fetch() -> Result { +/// Ok(reqwest::get("https://www.rust-lang.org").await?.text().await?) +/// } +/// +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let content = fetch.retry(ExponentialBackoff::default()).await?; +/// println!("fetch succeeded: {}", content); +/// +/// Ok(()) +/// } +/// ``` pub trait Retryable< P: Backoff, T, @@ -110,44 +157,23 @@ where #[cfg(test)] mod tests { + use std::time::Duration; + use super::*; use crate::exponential::ExponentialBackoff; - #[tokio::test] - async fn test_retry() -> anyhow::Result<()> { - let x = { - || async { - let x = reqwest::get("https://www.rust-lang.org") - .await? - .text() - .await?; - - Err(anyhow::anyhow!(x)) - } - } - .retry(ExponentialBackoff::default()) - .await?; - - println!("got: {:?}", x); - - Ok(()) - } - - async fn test_query() -> anyhow::Result<()> { - let x = reqwest::get("https://www.rust-lang.org") - .await? - .text() - .await?; - - Err(anyhow::anyhow!(x)) + async fn always_error() -> anyhow::Result<()> { + Err(anyhow::anyhow!("test_query meets error")) } #[tokio::test] async fn test_retry_x() -> anyhow::Result<()> { - let x = test_query.retry(ExponentialBackoff::default()).await?; - - println!("got: {:?}", x); + let result = always_error + .retry(ExponentialBackoff::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(()) } }