From 7ffcbd0714672495e29870a6c30da7051681948d Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Sat, 31 Aug 2024 00:45:22 +0800 Subject: [PATCH] feat: Add blocking sleeper for blocking retry (#138) Signed-off-by: Xuanwo --- backon/Cargo.toml | 3 +- backon/src/blocking_retry.rs | 71 ++++++++++++++++++++--- backon/src/blocking_retry_with_context.rs | 49 +++++++++++++--- backon/src/blocking_sleep.rs | 55 ++++++++++++++++++ backon/src/lib.rs | 6 ++ 5 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 backon/src/blocking_sleep.rs diff --git a/backon/Cargo.toml b/backon/Cargo.toml index cd7e2fa..e83feb9 100644 --- a/backon/Cargo.toml +++ b/backon/Cargo.toml @@ -20,7 +20,8 @@ targets = [ ] [features] -default = ["tokio-sleep", "gloo-timers-sleep"] +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"] diff --git a/backon/src/blocking_retry.rs b/backon/src/blocking_retry.rs index 72552c0..cb901ce 100644 --- a/backon/src/blocking_retry.rs +++ b/backon/src/blocking_retry.rs @@ -1,8 +1,8 @@ -use std::thread; use std::time::Duration; use crate::backoff::BackoffBuilder; -use crate::Backoff; +use crate::blocking_sleep::MaybeBlockingSleeper; +use crate::{Backoff, BlockingSleeper, DefaultBlockingSleeper}; /// BlockingRetryable adds retry support for blocking functions. /// @@ -63,6 +63,7 @@ pub struct BlockingRetry< T, E, F: FnMut() -> Result, + SF: MaybeBlockingSleeper = DefaultBlockingSleeper, RF = fn(&E) -> bool, NF = fn(&E, Duration), > { @@ -70,6 +71,7 @@ pub struct BlockingRetry< retryable: RF, notify: NF, f: F, + sleep_fn: SF, } impl BlockingRetry @@ -83,18 +85,57 @@ where backoff, retryable: |_: &E| true, notify: |_: &E, _: Duration| {}, + sleep_fn: DefaultBlockingSleeper::default(), f, } } } -impl BlockingRetry +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. @@ -120,12 +161,16 @@ where /// Ok(()) /// } /// ``` - pub fn when bool>(self, retryable: RN) -> BlockingRetry { + pub fn when bool>( + self, + retryable: RN, + ) -> BlockingRetry { BlockingRetry { backoff: self.backoff, retryable, notify: self.notify, f: self.f, + sleep_fn: self.sleep_fn, } } @@ -160,15 +205,28 @@ where /// Ok(()) /// } /// ``` - pub fn notify(self, notify: NN) -> BlockingRetry { + 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 [`std::ops::FnOnce`] after it stable. @@ -187,7 +245,7 @@ where None => return Err(err), Some(dur) => { (self.notify)(&err, dur); - thread::sleep(dur); + self.sleep_fn.sleep(dur); } } } @@ -195,7 +253,6 @@ where } } } - #[cfg(test)] mod tests { use std::sync::Mutex; diff --git a/backon/src/blocking_retry_with_context.rs b/backon/src/blocking_retry_with_context.rs index 5ce6d77..1bb1db1 100644 --- a/backon/src/blocking_retry_with_context.rs +++ b/backon/src/blocking_retry_with_context.rs @@ -1,8 +1,8 @@ -use std::thread; use std::time::Duration; use crate::backoff::BackoffBuilder; -use crate::Backoff; +use crate::blocking_sleep::MaybeBlockingSleeper; +use crate::{Backoff, BlockingSleeper, DefaultBlockingSleeper}; /// BlockingRetryableWithContext adds retry support for blocking functions. pub trait BlockingRetryableWithContext< @@ -34,6 +34,7 @@ pub struct BlockingRetryWithContext< E, Ctx, F: FnMut(Ctx) -> (Ctx, Result), + SF: MaybeBlockingSleeper = DefaultBlockingSleeper, RF = fn(&E) -> bool, NF = fn(&E, Duration), > { @@ -41,6 +42,7 @@ pub struct BlockingRetryWithContext< retryable: RF, notify: NF, f: F, + sleep_fn: SF, ctx: Option, } @@ -55,44 +57,67 @@ where backoff, retryable: |_: &E| true, notify: |_: &E, _: Duration| {}, + sleep_fn: DefaultBlockingSleeper::default(), f, ctx: None, } } } -impl BlockingRetryWithContext +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 { + 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 { BlockingRetryWithContext { backoff: self.backoff, retryable, notify: self.notify, f: self.f, + sleep_fn: self.sleep_fn, ctx: self.ctx, } } @@ -105,16 +130,26 @@ where pub fn notify( self, notify: NN, - ) -> BlockingRetryWithContext { + ) -> 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 [`std::ops::FnOnce`] after it stable. @@ -136,7 +171,7 @@ where None => return (ctx, Err(err)), Some(dur) => { (self.notify)(&err, dur); - thread::sleep(dur); + self.sleep_fn.sleep(dur); } } } diff --git a/backon/src/blocking_sleep.rs b/backon/src/blocking_sleep.rs new file mode 100644 index 0000000..72b8ebe --- /dev/null +++ b/backon/src/blocking_sleep.rs @@ -0,0 +1,55 @@ +use std::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(all(not(feature = "tokio-sleep"), not(feature = "gloo-timers-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) + } +} diff --git a/backon/src/lib.rs b/backon/src/lib.rs index a5434a9..917be53 100644 --- a/backon/src/lib.rs +++ b/backon/src/lib.rs @@ -137,5 +137,11 @@ mod blocking_retry_with_context; pub use blocking_retry_with_context::BlockingRetryWithContext; pub use blocking_retry_with_context::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;