Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
use core::time::Duration;
/// 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<Item = Duration> + Send + Sync + Unpin {}
impl<T> Backoff for T where T: Iterator<Item = Duration> + Send + Sync + Unpin {}
/// BackoffBuilder is utilized to construct a new backoff.
pub trait BackoffBuilder: 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;
}
impl<B: Backoff> BackoffBuilder for B {
type Backoff = B;
fn build(self) -> Self::Backoff {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ConstantBuilder;
use crate::ExponentialBuilder;
use crate::FibonacciBuilder;
fn test_fn_builder(b: impl BackoffBuilder) {
let _ = b.build();
}
#[test]
fn test_backoff_builder() {
test_fn_builder([Duration::from_secs(1)].into_iter());
// Just for test if user can keep using &XxxBuilder.
#[allow(clippy::needless_borrows_for_generic_args)]
{
test_fn_builder(&ConstantBuilder::default());
test_fn_builder(&FibonacciBuilder::default());
test_fn_builder(&ExponentialBuilder::default());
}
}
}
+235
View File
@@ -0,0 +1,235 @@
use 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<String> {
/// 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<usize>,
jitter: bool,
seed: Option<u64>,
}
impl Default for ConstantBuilder {
fn default() -> Self {
Self::new()
}
}
impl ConstantBuilder {
/// Create a new `ConstantBuilder` with default values.
pub const fn new() -> Self {
Self {
delay: Duration::from_secs(1),
max_times: Some(3),
jitter: false,
seed: None,
}
}
/// Set the delay for the backoff.
pub const fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
/// Set the maximum number of attempts to be made.
pub const fn with_max_times(mut self, max_times: usize) -> Self {
self.max_times = Some(max_times);
self
}
/// Enable jitter for the backoff.
///
/// Jitter is a random value added to the delay to prevent a thundering herd problem.
pub const fn with_jitter(mut self) -> Self {
self.jitter = true;
self
}
/// Set the seed value for the jitter random number generator. If no seed is given, a random seed is used in std and default seed is used in no_std.
pub fn with_jitter_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
/// Set no max times for the backoff.
///
/// The backoff will not stop by itself.
///
/// _The backoff could stop reaching `usize::MAX` attempts but this is **unrealistic**._
pub const fn without_max_times(mut self) -> Self {
self.max_times = None;
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,
rng: if let Some(seed) = self.seed {
fastrand::Rng::with_seed(seed)
} else {
#[cfg(feature = "std")]
let rng = fastrand::Rng::new();
#[cfg(not(feature = "std"))]
let rng = fastrand::Rng::with_seed(super::RANDOM_SEED);
rng
},
}
}
}
impl BackoffBuilder for &ConstantBuilder {
type Backoff = ConstantBackoff;
fn build(self) -> Self::Backoff {
(*self).build()
}
}
/// 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<usize>,
attempts: usize,
jitter: bool,
rng: fastrand::Rng,
}
impl Iterator for ConstantBackoff {
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
let mut delay = || match self.jitter {
true => self.delay + self.delay.mul_f32(self.rng.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 super::*;
const TEST_BUILDER: ConstantBuilder = ConstantBuilder::new()
.with_delay(Duration::from_secs(2))
.with_max_times(5)
.with_jitter();
#[test]
fn test_constant_default() {
let mut it = ConstantBuilder::default().build();
assert_eq!(Some(Duration::from_secs(1)), it.next());
assert_eq!(Some(Duration::from_secs(1)), it.next());
assert_eq!(Some(Duration::from_secs(1)), it.next());
assert_eq!(None, it.next());
}
#[test]
fn test_constant_with_delay() {
let mut it = ConstantBuilder::default()
.with_delay(Duration::from_secs(2))
.build();
assert_eq!(Some(Duration::from_secs(2)), it.next());
assert_eq!(Some(Duration::from_secs(2)), it.next());
assert_eq!(Some(Duration::from_secs(2)), it.next());
assert_eq!(None, it.next());
}
#[test]
fn test_constant_with_times() {
let mut it = ConstantBuilder::default().with_max_times(1).build();
assert_eq!(Some(Duration::from_secs(1)), it.next());
assert_eq!(None, it.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));
}
#[test]
fn test_constant_without_max_times() {
let mut it = ConstantBuilder::default().without_max_times().build();
for _ in 0..10_000 {
assert_eq!(Some(Duration::from_secs(1)), it.next());
}
}
// allow assertions on constants because they are not optimized out by unit tests
#[allow(clippy::assertions_on_constants)]
#[test]
fn test_constant_const_builder() {
assert_eq!(TEST_BUILDER.delay, Duration::from_secs(2));
assert_eq!(TEST_BUILDER.max_times, Some(5));
assert!(TEST_BUILDER.jitter);
}
}
+461
View File
@@ -0,0 +1,461 @@
use 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<String> {
/// 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<Duration>,
max_times: Option<usize>,
total_delay: Option<Duration>,
seed: Option<u64>,
}
impl Default for ExponentialBuilder {
fn default() -> Self {
Self::new()
}
}
impl ExponentialBuilder {
/// Create a new `ExponentialBuilder` with default values.
pub const fn new() -> Self {
Self {
jitter: false,
factor: 2.0,
min_delay: Duration::from_secs(1),
max_delay: Some(Duration::from_secs(60)),
max_times: Some(3),
total_delay: None,
seed: None,
}
}
/// Enable jitter for the backoff.
///
/// When jitter is enabled, [`ExponentialBackoff`] will add a random jitter within `(0, current_delay)`
/// to the current delay.
pub const fn with_jitter(mut self) -> Self {
self.jitter = true;
self
}
/// Set the seed value for the jitter random number generator. If no seed is given, a random seed is used in std and default seed is used in no_std.
pub fn with_jitter_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
/// Set the factor for the backoff.
///
/// Note: Having a factor less than `1.0` does not make any sense as it would create a
/// smaller negative backoff.
pub const fn with_factor(mut self, factor: f32) -> Self {
self.factor = factor;
self
}
/// Set the minimum delay for the backoff.
pub const 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 const fn with_max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = Some(max_delay);
self
}
/// Set no maximum delay for the backoff.
///
/// The delay will keep increasing.
///
/// _The delay will saturate at `Duration::MAX` which is an **unrealistic** delay._
pub const fn without_max_delay(mut self) -> Self {
self.max_delay = None;
self
}
/// Set the maximum number of attempts for the current backoff.
///
/// The backoff will stop if the maximum number of attempts is reached.
pub const fn with_max_times(mut self, max_times: usize) -> Self {
self.max_times = Some(max_times);
self
}
/// Set no maximum number of attempts for the current backoff.
///
/// The backoff will not stop by itself.
///
/// _The backoff could stop reaching `usize::MAX` attempts but this is **unrealistic**._
pub const fn without_max_times(mut self) -> Self {
self.max_times = None;
self
}
/// Set the total delay for the backoff.
///
/// The backoff will stop yielding sleep durations once the cumulative sleep time
/// plus the next sleep duration would exceed `total_delay`.
pub const fn with_total_delay(mut self, total_delay: Option<Duration>) -> Self {
self.total_delay = total_delay;
self
}
}
impl BackoffBuilder for ExponentialBuilder {
type Backoff = ExponentialBackoff;
fn build(self) -> Self::Backoff {
ExponentialBackoff {
jitter: self.jitter,
rng: if let Some(seed) = self.seed {
fastrand::Rng::with_seed(seed)
} else {
#[cfg(feature = "std")]
let rng = fastrand::Rng::new();
#[cfg(not(feature = "std"))]
let rng = fastrand::Rng::with_seed(super::RANDOM_SEED);
rng
},
factor: self.factor,
min_delay: self.min_delay,
max_delay: self.max_delay,
max_times: self.max_times,
current_delay: None,
attempts: 0,
cumulative_delay: Duration::ZERO,
total_delay: self.total_delay,
}
}
}
impl BackoffBuilder for &ExponentialBuilder {
type Backoff = ExponentialBackoff;
fn build(self) -> Self::Backoff {
(*self).build()
}
}
/// ExponentialBackoff provides a delay with exponential retries.
///
/// This backoff strategy is constructed by [`ExponentialBuilder`].
#[doc(hidden)]
#[derive(Debug)]
pub struct ExponentialBackoff {
jitter: bool,
rng: fastrand::Rng,
factor: f32,
min_delay: Duration,
max_delay: Option<Duration>,
max_times: Option<usize>,
total_delay: Option<Duration>,
current_delay: Option<Duration>,
cumulative_delay: Duration,
attempts: usize,
}
impl Iterator for ExponentialBackoff {
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
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.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);
}
cur
}
};
let current_delay = tmp_cur;
// If jitter is enabled, add random jitter based on min delay.
if self.jitter {
tmp_cur = tmp_cur.saturating_add(tmp_cur.mul_f32(self.rng.f32()));
}
// Check if adding the current delay would exceed the total delay limit.
let total_delay_check = self
.total_delay
.is_none_or(|total| self.cumulative_delay + tmp_cur <= total);
if !total_delay_check {
return None;
}
if self.total_delay.is_some() {
self.cumulative_delay = self.cumulative_delay.saturating_add(tmp_cur);
}
self.current_delay = Some(current_delay);
Some(tmp_cur)
}
}
#[inline]
pub(crate) fn saturating_mul(d: Duration, rhs: f32) -> Duration {
Duration::try_from_secs_f32(rhs * d.as_secs_f32()).unwrap_or(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;
const TEST_BUILDER: ExponentialBuilder = ExponentialBuilder::new()
.with_jitter()
.with_factor(1.5)
.with_min_delay(Duration::from_secs(2))
.with_max_delay(Duration::from_secs(30))
.with_max_times(5);
#[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_total_delay() {
let mut exp = ExponentialBuilder::default()
.with_min_delay(Duration::from_secs(1))
.with_factor(1.0)
.with_total_delay(Some(Duration::from_secs(3)))
.with_max_times(5)
.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_exponential_no_max_times_with_default() {
let mut exp = ExponentialBuilder::default()
.with_min_delay(Duration::from_secs(1))
.with_factor(1_f32)
.without_max_times()
.build();
// to fully test we would need to call this `usize::MAX`
// which seems unreasonable for a test as it would take too long...
for _ in 0..10_000 {
assert_eq!(Some(Duration::from_secs(1)), 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_no_max_delay_with_default() {
let mut exp = ExponentialBuilder::default()
.with_min_delay(Duration::from_secs(1))
.with_factor(10_000_000_000_f32)
.without_max_delay()
.with_max_times(4)
.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());
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_max_delay_without_default_1() {
let mut exp = ExponentialBuilder {
jitter: false,
seed: Some(0x2fdb0020ffc7722b),
factor: 10_000_000_000_f32,
min_delay: Duration::from_secs(1),
max_delay: None,
max_times: None,
total_delay: 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,
seed: Some(0x2fdb0020ffc7722b),
factor: 10_000_000_000_f32,
min_delay: Duration::from_secs(10_000_000_000),
max_delay: None,
max_times: Some(2),
total_delay: None,
}
.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,
seed: Some(0x2fdb0020ffc7722b),
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),
total_delay: None,
}
.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());
}
// allow assertions on constants because they are not optimized out by unit tests
#[allow(clippy::assertions_on_constants)]
#[test]
fn test_exponential_const_builder() {
assert!(TEST_BUILDER.jitter);
assert_eq!(TEST_BUILDER.factor, 1.5);
assert_eq!(TEST_BUILDER.min_delay, Duration::from_secs(2));
assert_eq!(TEST_BUILDER.max_delay, Some(Duration::from_secs(30)));
assert_eq!(TEST_BUILDER.max_times, Some(5));
}
}
+345
View File
@@ -0,0 +1,345 @@
use 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<String> {
/// 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,
seed: Option<u64>,
min_delay: Duration,
max_delay: Option<Duration>,
max_times: Option<usize>,
}
impl Default for FibonacciBuilder {
fn default() -> Self {
Self::new()
}
}
impl FibonacciBuilder {
/// Create a new `FibonacciBuilder` with default values.
pub const fn new() -> Self {
Self {
jitter: false,
seed: None,
min_delay: Duration::from_secs(1),
max_delay: Some(Duration::from_secs(60)),
max_times: Some(3),
}
}
/// Set the jitter for the backoff.
///
/// When jitter is enabled, FibonacciBackoff will add a random jitter between `(0, current_delay)` to the delay.
pub const fn with_jitter(mut self) -> Self {
self.jitter = true;
self
}
/// Set the seed value for the jitter random number generator. If no seed is given, a random seed is used in std and default seed is used in no_std.
pub fn with_jitter_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
/// Set the minimum delay for the backoff.
pub const 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 const fn with_max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = Some(max_delay);
self
}
/// Set no maximum delay for the backoff.
///
/// The delay will keep increasing.
///
/// _The delay will saturate at `Duration::MAX` which is an **unrealistic** delay._
pub const fn without_max_delay(mut self) -> Self {
self.max_delay = None;
self
}
/// Set the maximum number of attempts for the current backoff.
///
/// The backoff will stop if the maximum number of attempts is reached.
pub const fn with_max_times(mut self, max_times: usize) -> Self {
self.max_times = Some(max_times);
self
}
/// Set no maximum number of attempts for the current backoff.
///
/// The backoff will not stop by itself.
///
/// _The backoff could stop reaching `usize::MAX` attempts but this is **unrealistic**._
pub const fn without_max_times(mut self) -> Self {
self.max_times = None;
self
}
}
impl BackoffBuilder for FibonacciBuilder {
type Backoff = FibonacciBackoff;
fn build(self) -> Self::Backoff {
FibonacciBackoff {
jitter: self.jitter,
rng: if let Some(seed) = self.seed {
fastrand::Rng::with_seed(seed)
} else {
#[cfg(feature = "std")]
let rng = fastrand::Rng::new();
#[cfg(not(feature = "std"))]
let rng = fastrand::Rng::with_seed(super::RANDOM_SEED);
rng
},
min_delay: self.min_delay,
max_delay: self.max_delay,
max_times: self.max_times,
previous_delay: None,
current_delay: None,
attempts: 0,
}
}
}
impl BackoffBuilder for &FibonacciBuilder {
type Backoff = FibonacciBackoff;
fn build(self) -> Self::Backoff {
(*self).build()
}
}
/// FibonacciBackoff offers a delay with Fibonacci-based retries.
///
/// This backoff strategy is constructed by [`FibonacciBuilder`].
#[doc(hidden)]
#[derive(Debug)]
pub struct FibonacciBackoff {
jitter: bool,
rng: fastrand::Rng,
min_delay: Duration,
max_delay: Option<Duration>,
max_times: Option<usize>,
previous_delay: Option<Duration>,
current_delay: Option<Duration>,
attempts: usize,
}
impl Iterator for FibonacciBackoff {
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
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 += next.mul_f32(self.rng.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 = next.saturating_add(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(self.rng.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 super::*;
const TEST_BUILDER: FibonacciBuilder = FibonacciBuilder::new()
.with_jitter()
.with_min_delay(Duration::from_secs(2))
.with_max_delay(Duration::from_secs(30))
.with_max_times(5);
#[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_no_max_delay() {
let mut fib = FibonacciBuilder::default()
.with_max_times(4)
.with_min_delay(Duration::from_secs(10_000_000_000_000_000_000))
.without_max_delay()
.build();
assert_eq!(
Some(Duration::from_secs(10_000_000_000_000_000_000)),
fib.next()
);
assert_eq!(
Some(Duration::from_secs(10_000_000_000_000_000_000)),
fib.next()
);
assert_eq!(Some(Duration::MAX), fib.next());
assert_eq!(Some(Duration::MAX), 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());
}
#[test]
fn test_fibonacci_no_max_times() {
let mut fib = FibonacciBuilder::default()
.with_min_delay(Duration::from_secs(0))
.without_max_times()
.build();
// to fully test we would need to call this `usize::MAX`
// which seems unreasonable for a test as it would take too long...
for _ in 0..10_000 {
assert_eq!(Some(Duration::from_secs(0)), fib.next());
}
}
// allow assertions on constants because they are not optimized out by unit tests
#[allow(clippy::assertions_on_constants)]
#[test]
fn test_fibonacci_const_builder() {
assert!(TEST_BUILDER.jitter);
assert_eq!(TEST_BUILDER.min_delay, Duration::from_secs(2));
assert_eq!(TEST_BUILDER.max_delay, Some(Duration::from_secs(30)));
assert_eq!(TEST_BUILDER.max_times, Some(5));
}
}
+18
View File
@@ -0,0 +1,18 @@
mod 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;
// Random seed value for no_std (the value is "backon" in hex)
#[cfg(not(feature = "std"))]
const RANDOM_SEED: u64 = 0x6261636b6f6e;
+365
View File
@@ -0,0 +1,365 @@
use core::time::Duration;
use crate::Backoff;
use crate::BlockingSleeper;
use crate::DefaultBlockingSleeper;
use crate::backoff::BackoffBuilder;
use crate::blocking_sleep::MaybeBlockingSleeper;
/// BlockingRetryable adds retry support for blocking functions.
///
/// For example:
///
/// - Functions without extra args:
///
/// ```ignore
/// fn fetch() -> Result<String> {
/// 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<String> {
/// Ok("hello, world!".to_string())
/// }
///
/// fn main() -> Result<()> {
/// let content = fetch.retry(ExponentialBuilder::default()).call()?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
pub trait BlockingRetryable<B: BackoffBuilder, T, E, F: FnMut() -> Result<T, E>> {
/// Generate a new retry.
fn retry(self, builder: B) -> BlockingRetry<B::Backoff, T, E, F>;
}
impl<B, T, E, F> BlockingRetryable<B, T, E, F> for F
where
B: BackoffBuilder,
F: FnMut() -> Result<T, E>,
{
fn retry(self, builder: B) -> BlockingRetry<B::Backoff, T, E, F> {
BlockingRetry::new(self, builder.build())
}
}
/// Retry structure generated by [`BlockingRetryable`].
pub struct BlockingRetry<
B: Backoff,
T,
E,
F: FnMut() -> Result<T, E>,
SF: MaybeBlockingSleeper = DefaultBlockingSleeper,
RF = fn(&E) -> bool,
NF = fn(&E, Duration),
> {
backoff: B,
retryable: RF,
notify: NF,
f: F,
sleep_fn: SF,
}
impl<B, T, E, F> BlockingRetry<B, T, E, F>
where
B: Backoff,
F: FnMut() -> Result<T, E>,
{
/// 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<B, T, E, F, SF, RF, NF> BlockingRetry<B, T, E, F, SF, RF, NF>
where
B: Backoff,
F: FnMut() -> Result<T, E>,
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<String> {
/// 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<SN: BlockingSleeper>(self, sleep_fn: SN) -> BlockingRetry<B, T, E, F, SN, RF, NF> {
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<String> {
/// 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<RN: FnMut(&E) -> bool>(
self,
retryable: RN,
) -> BlockingRetry<B, T, E, F, SF, RN, NF> {
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<String> {
/// 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<NN: FnMut(&E, Duration)>(
self,
notify: NN,
) -> BlockingRetry<B, T, E, F, SF, RF, NN> {
BlockingRetry {
backoff: self.backoff,
retryable: self.retryable,
notify,
f: self.f,
sleep_fn: self.sleep_fn,
}
}
}
impl<B, T, E, F, SF, RF, NF> BlockingRetry<B, T, E, F, SF, RF, NF>
where
B: Backoff,
F: FnMut() -> Result<T, E>,
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<T, E> {
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 {
extern crate alloc;
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(())
}
}
@@ -0,0 +1,237 @@
use core::time::Duration;
use crate::Backoff;
use crate::BlockingSleeper;
use crate::DefaultBlockingSleeper;
use crate::backoff::BackoffBuilder;
use crate::blocking_sleep::MaybeBlockingSleeper;
/// BlockingRetryableWithContext adds retry support for blocking functions.
pub trait BlockingRetryableWithContext<
B: BackoffBuilder,
T,
E,
Ctx,
F: FnMut(Ctx) -> (Ctx, Result<T, E>),
>
{
/// Generate a new retry
fn retry(self, builder: B) -> BlockingRetryWithContext<B::Backoff, T, E, Ctx, F>;
}
impl<B, T, E, Ctx, F> BlockingRetryableWithContext<B, T, E, Ctx, F> for F
where
B: BackoffBuilder,
F: FnMut(Ctx) -> (Ctx, Result<T, E>),
{
fn retry(self, builder: B) -> BlockingRetryWithContext<B::Backoff, T, E, Ctx, F> {
BlockingRetryWithContext::new(self, builder.build())
}
}
/// Retry structure generated by [`BlockingRetryableWithContext`].
pub struct BlockingRetryWithContext<
B: Backoff,
T,
E,
Ctx,
F: FnMut(Ctx) -> (Ctx, Result<T, E>),
SF: MaybeBlockingSleeper = DefaultBlockingSleeper,
RF = fn(&E) -> bool,
NF = fn(&E, Duration),
> {
backoff: B,
retryable: RF,
notify: NF,
f: F,
sleep_fn: SF,
ctx: Option<Ctx>,
}
impl<B, T, E, Ctx, F> BlockingRetryWithContext<B, T, E, Ctx, F>
where
B: Backoff,
F: FnMut(Ctx) -> (Ctx, Result<T, E>),
{
/// 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<B, T, E, Ctx, F, SF, RF, NF> BlockingRetryWithContext<B, T, E, Ctx, F, SF, RF, NF>
where
B: Backoff,
F: FnMut(Ctx) -> (Ctx, Result<T, E>),
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<B, T, E, Ctx, F, SF, RF, NF> {
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<SN: BlockingSleeper>(
self,
sleep_fn: SN,
) -> BlockingRetryWithContext<B, T, E, Ctx, F, SN, RF, NF> {
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<RN: FnMut(&E) -> bool>(
self,
retryable: RN,
) -> BlockingRetryWithContext<B, T, E, Ctx, F, SF, RN, NF> {
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<NN: FnMut(&E, Duration)>(
self,
notify: NN,
) -> BlockingRetryWithContext<B, T, E, Ctx, F, SF, RF, NN> {
BlockingRetryWithContext {
backoff: self.backoff,
retryable: self.retryable,
notify,
f: self.f,
sleep_fn: self.sleep_fn,
ctx: self.ctx,
}
}
}
impl<B, T, E, Ctx, F, SF, RF, NF> BlockingRetryWithContext<B, T, E, Ctx, F, SF, RF, NF>
where
B: Backoff,
F: FnMut(Ctx) -> (Ctx, Result<T, E>),
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<T, E>) {
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 {
extern crate alloc;
use alloc::string::ToString;
use core::time::Duration;
use anyhow::Result;
use anyhow::anyhow;
use spin::Mutex;
use super::*;
use crate::ExponentialBuilder;
struct Test;
impl Test {
fn hello(&mut self) -> Result<usize> {
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(())
}
}
+56
View File
@@ -0,0 +1,56 @@
use 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<T: BlockingSleeper + ?Sized> MaybeBlockingSleeper for T {}
/// All `Fn(Duration)` implements `Sleeper`.
impl<F: Fn(Duration) + 'static> 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)]
#[allow(dead_code)]
#[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)
}
}
+19
View File
@@ -0,0 +1,19 @@
Retry an async function.
```rust
use backon::ExponentialBuilder;
use backon::Retryable;
use anyhow::Result;
async fn fetch() -> Result<String> {
Ok("Hello, World!".to_string())
}
#[tokio::main]
async fn main() -> Result<()> {
let content = fetch.retry(ExponentialBuilder::default()).await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
@@ -0,0 +1,17 @@
Retry 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::<u32, anyhow::Error>(var);
let result = f.retry(backon::ExponentialBuilder::default()).call()?;
println!("var = {result}");
Ok(())
}
```
@@ -0,0 +1,43 @@
Let's implement a custom async Sleeper, say you are using Monoio as your async
runtime, you may want to implement it with `monoio::time::sleep()`. If you want
to implement a custom blocking Sleeper, you will find it pretty similar.
```rust
use std::time::Duration;
use backon::Sleeper;
/// Sleeper implemented using `monoio::time::sleep()`.
struct MonoioSleeper;
impl Sleeper for MonoioSleeper {
type Sleep = monoio::time::Sleep;
fn sleep(&self, dur: Duration) -> Self::Sleep {
monoio::time::sleep(dur)
}
}
```
Then you can use it like:
```rust
use backon::ExponentialBuilder;
use backon::Retryable;
use anyhow::Result;
async fn fetch() -> Result<String> {
Ok("Hello, World!".to_string())
}
#[monoio::main(timer_enabled = true)]
async fn main() -> Result<()> {
let content = fetch
.retry(ExponentialBuilder::default())
.sleep(MonoioSleeper)
.await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
@@ -0,0 +1,23 @@
Retry 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<String> {
Ok(reqwest::get(url).await?.text().await?)
}
async fn run(&mut self) -> Result<String> {
let content = (|| async { self.fetch("https://www.rust-lang.org").await })
.retry(ExponentialBuilder::default())
.when(|e| e.to_string() == "retryable")
.await?;
Ok(content)
}
}
```
+28
View File
@@ -0,0 +1,28 @@
//! 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 {}
#[doc = include_str!("retry_after.md")]
pub mod retry_after {}
@@ -0,0 +1,63 @@
Retry an async function with the `Retry-After` headers.
```no_run
use core::time::Duration;
use std::error::Error;
use std::fmt::Display;
use std::fmt::Formatter;
use anyhow::Result;
use backon::ExponentialBuilder;
use backon::Retryable;
use reqwest::header::HeaderMap;
use reqwest::StatusCode;
#[derive(Debug)]
struct HttpError {
headers: HeaderMap,
}
impl Display for HttpError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "http error")
}
}
impl Error for HttpError {}
async fn fetch() -> Result<String> {
let resp = reqwest::get("https://www.rust-lang.org").await?;
if resp.status() != StatusCode::OK {
let source = HttpError {
headers: resp.headers().clone(),
};
return Err(anyhow::Error::new(source));
}
Ok(resp.text().await?)
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let content = fetch
.retry(ExponentialBuilder::default())
.adjust(|err, dur| {
match err.downcast_ref::<HttpError>() {
Some(v) => {
if let Some(retry_after) = v.headers.get("Retry-After") {
// Parse the Retry-After header and adjust the backoff duration
let retry_after = retry_after.to_str().unwrap_or("0");
let retry_after = retry_after.parse::<u64>().unwrap_or(0);
Some(Duration::from_secs(retry_after))
} else {
dur
}
}
None => dur,
}
})
.await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
+23
View File
@@ -0,0 +1,23 @@
Retry 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(())
}
```
@@ -0,0 +1,23 @@
Retry an async function in a wasm32 environment using `backon` for exponential backoff, and `wasm-bindgen` + `spawn_local` to run async code in the browser.
```rust
use anyhow::Result;
use backon::{ExponentialBuilder, Retryable};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
async fn fetch() -> Result<String> {
Ok("Hello, wasm32!".to_string())
}
#[wasm_bindgen(start)]
fn start() {
spawn_local(async {
match fetch.retry(ExponentialBuilder::default()).await {
Ok(content) => web_sys::console::log_1(&format!("fetch succeeded: {}", content).into()),
Err(e) => web_sys::console::error_1(&format!("fetch failed: {:?}", e).into()),
}
});
}
```
@@ -0,0 +1,24 @@
Retry 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<String> {
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(())
}
```
@@ -0,0 +1,36 @@
Retry 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<String> {
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(())
}
```
@@ -0,0 +1,28 @@
Retry 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<String> {
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(())
}
```
@@ -0,0 +1,21 @@
Retry with specify retryable error by `when`.
```rust
use anyhow::Result;
use backon::ExponentialBuilder;
use backon::Retryable;
async fn fetch() -> Result<String> {
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(())
}
```
+3
View File
@@ -0,0 +1,3 @@
//! Docs for the backon crate, like [`examples`].
pub mod examples;
+22
View File
@@ -0,0 +1,22 @@
use core::time::Duration;
use crate::BlockingSleeper;
use crate::Sleeper;
/// A no_std async sleeper based on the embassy framework (https://embassy.dev)
#[derive(Clone, Copy, Debug, Default)]
pub struct EmbassySleeper;
impl Sleeper for EmbassySleeper {
type Sleep = embassy_time::Timer;
fn sleep(&self, dur: Duration) -> Self::Sleep {
embassy_time::Timer::after_millis(dur.as_millis() as u64)
}
}
impl BlockingSleeper for EmbassySleeper {
fn sleep(&self, dur: Duration) {
embassy_time::block_for(embassy_time::Duration::from_millis(dur.as_millis() as u64));
}
}
+258
View File
@@ -0,0 +1,258 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/Xuanwo/backon/main/.github/assets/logo.jpeg"
)]
#![cfg_attr(docsrs, feature(doc_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
//!
//! <img src="https://raw.githubusercontent.com/Xuanwo/backon/main/.github/assets/logo.jpeg" alt="BackON" width="38.2%"/>
//!
//! 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. It also accepts any object that implements [`Backoff`]. You can therefore easily implement your own custom backoff strategy.
//!
//! 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, such an implementation
//! is called a Sleeper, it will implement [`Sleeper`] or [`BlockingSleeper`] depending
//! on if it is going to be used in an asynchronous context.
//!
//! ## Default Sleeper
//!
//! Currently, BackON has 3 built-in Sleeper implementations for different
//! environments, they are gated under their own features, which are enabled
//! by default:
//!
//! | `Sleeper` | feature | Environment | Asynchronous |
//! |-------------------------|---------------------|-------------|---------------|
//! | [`TokioSleeper`] | tokio-sleep | non-wasm32 | Yes |
//! | [`GlooTimersSleep`] | gloo-timers-sleep | wasm32 | Yes |
//! | [`FuturesTimerSleeper`] | futures-timer-sleep |wasm/non-wasm| Yes |
//! | [`EmbassySleep`] | embassy-sleep | no_std | Yes |
//! | [`StdSleeper`] | std-blocking-sleep | std | No |
//!
//! ## Custom Sleeper
//!
//! If you do not want to use the built-in Sleeper, you CAN provide a custom
//! implementation, let's implement an asynchronous dummy Sleeper that does
//! not sleep at all. You will find it pretty similar when you implement a
//! blocking one.
//!
//! ```
//! use std::time::Duration;
//!
//! use backon::Sleeper;
//!
//! /// A dummy `Sleeper` impl that prints then becomes ready!
//! struct DummySleeper;
//!
//! impl Sleeper for DummySleeper {
//! type Sleep = std::future::Ready<()>;
//!
//! fn sleep(&self, dur: Duration) -> Self::Sleep {
//! println!("Hello from DummySleeper!");
//! std::future::ready(())
//! }
//! }
//! ```
//!
//! ## The empty Sleeper
//!
//! If neither feature is enabled nor a custom implementation is provided, BackON
//! will fallback to the empty sleeper, in which case, a compile-time error that
//! `PleaseEnableAFeatureOrProvideACustomSleeper needs to implement Sleeper or
//! BlockingSleeper` will be raised to remind you to choose or bring a real Sleeper
//! implementation.
//!
//! # 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<String> {
//! Ok("hello, world!".to_string())
//! }
//!
//! #[tokio::main(flavor = "current_thread")]
//! 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<String> {
//! Ok("hello, world!".to_string())
//! }
//!
//! fn main() -> Result<()> {
//! let content = fetch
//! // Retry with exponential backoff
//! .retry(ExponentialBuilder::default())
//! // Sleep implementation, default to std::thread::sleep if `std-blocking-sleep` has been enabled.
//! .sleep(std::thread::sleep)
//! // 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(())
//! }
//! ```
//!
//! ## Retry an async function with context
//!
//! Sometimes users can meet the problem that the async function is needs to take `FnMut`:
//!
//! ```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
//! ```
//!
//! `RetryableWithContext` is designed for this, it allows you to pass a context
//! to the retry function, and return it back after the retry is done.
//!
//! ```no_run
//! use anyhow::anyhow;
//! use anyhow::Result;
//! use backon::ExponentialBuilder;
//! use backon::RetryableWithContext;
//!
//! struct Test;
//!
//! impl Test {
//! async fn hello(&mut self) -> Result<usize> {
//! Err(anyhow!("not retryable"))
//! }
//! }
//!
//! #[tokio::main(flavor = "current_thread")]
//! async fn main() -> Result<()> {
//! let mut test = Test;
//!
//! // (Test, Result<usize>)
//! let (_, result) = {
//! |mut v: Test| async {
//! let res = v.hello().await;
//! (v, res)
//! }
//! }
//! .retry(ExponentialBuilder::default())
//! .context(test)
//! .await;
//!
//! Ok(())
//! }
//! ```
#![deny(missing_docs)]
#![deny(unused_qualifications)]
#![no_std]
#[cfg(feature = "std-blocking-sleep")]
extern crate std;
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(feature = "futures-timer-sleep")]
pub use sleep::FuturesTimerSleeper;
#[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;
pub use blocking_retry::BlockingRetryable;
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(feature = "embassy-sleep")]
mod embassy_timer_sleep;
#[cfg(feature = "embassy-sleep")]
pub use embassy_timer_sleep::EmbassySleeper;
#[cfg(docsrs)]
pub mod docs;
+587
View File
@@ -0,0 +1,587 @@
use core::future::Future;
use core::pin::Pin;
use core::task::Context;
use core::task::Poll;
use core::task::ready;
use core::time::Duration;
use crate::Backoff;
use crate::DefaultSleeper;
use crate::Sleeper;
use crate::backoff::BackoffBuilder;
use crate::sleep::MaybeSleeper;
/// Retryable will add retry support for functions that produce futures with results.
///
/// This means all types that implement `FnMut() -> impl Future<Output = Result<T, E>>`
/// will be able to use `retry`.
///
/// For example:
///
/// - Functions without extra args:
///
/// ```ignore
/// async fn fetch() -> Result<String> {
/// 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<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
>
{
/// Generate a new retry
fn retry(self, builder: B) -> Retry<B::Backoff, T, E, Fut, FutureFn>;
}
impl<B, T, E, Fut, FutureFn> Retryable<B, T, E, Fut, FutureFn> for FutureFn
where
B: BackoffBuilder,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
{
fn retry(self, builder: B) -> Retry<B::Backoff, T, E, Fut, FutureFn> {
Retry::new(self, builder.build())
}
}
/// Struct generated by [`Retryable`].
pub struct Retry<
B: Backoff,
T,
E,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
SF: MaybeSleeper = DefaultSleeper,
RF = fn(&E) -> bool,
NF = fn(&E, Duration),
AF = fn(&E, Option<Duration>) -> Option<Duration>,
> {
backoff: B,
future_fn: FutureFn,
retryable_fn: RF,
notify_fn: NF,
sleep_fn: SF,
adjust_fn: AF,
state: State<T, E, Fut, SF::Sleep>,
}
impl<B, T, E, Fut, FutureFn> Retry<B, T, E, Fut, FutureFn>
where
B: Backoff,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
{
/// Initiate a new retry.
fn new(future_fn: FutureFn, backoff: B) -> Self {
Retry {
backoff,
future_fn,
retryable_fn: |_: &E| true,
notify_fn: |_: &E, _: Duration| {},
adjust_fn: |_: &E, dur: Option<Duration>| dur,
sleep_fn: DefaultSleeper::default(),
state: State::Idle,
}
}
}
impl<B, T, E, Fut, FutureFn, SF, RF, NF, AF> Retry<B, T, E, Fut, FutureFn, SF, RF, NF, AF>
where
B: Backoff,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
SF: MaybeSleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
AF: FnMut(&E, Option<Duration>) -> Option<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 std::future::ready;
///
/// use anyhow::Result;
/// use backon::ExponentialBuilder;
/// use backon::Retryable;
///
/// async fn fetch() -> Result<String> {
/// 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<SN: Sleeper>(self, sleep_fn: SN) -> Retry<B, T, E, Fut, FutureFn, SN, RF, NF, AF> {
Retry {
backoff: self.backoff,
retryable_fn: self.retryable_fn,
notify_fn: self.notify_fn,
future_fn: self.future_fn,
sleep_fn,
adjust_fn: self.adjust_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<String> {
/// 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<RN: FnMut(&E) -> bool>(
self,
retryable: RN,
) -> Retry<B, T, E, Fut, FutureFn, SF, RN, NF, AF> {
Retry {
backoff: self.backoff,
retryable_fn: retryable,
notify_fn: self.notify_fn,
future_fn: self.future_fn,
sleep_fn: self.sleep_fn,
adjust_fn: self.adjust_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<String> {
/// 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<NN: FnMut(&E, Duration)>(
self,
notify: NN,
) -> Retry<B, T, E, Fut, FutureFn, SF, RF, NN, AF> {
Retry {
backoff: self.backoff,
retryable_fn: self.retryable_fn,
notify_fn: notify,
sleep_fn: self.sleep_fn,
future_fn: self.future_fn,
adjust_fn: self.adjust_fn,
state: self.state,
}
}
/// Sets the function to adjust the backoff duration for retry attempts.
///
/// When a retry occurs, the provided function will be called with the error and the proposed backoff duration, allowing you to modify the final duration used.
///
/// If the function returns `None`, it indicates that no further retries should be made, and the error will be returned regardless of the backoff duration provided by the input.
///
/// If no `adjust` function is specified, the original backoff duration from the input will be used without modification.
///
/// `adjust` can be used to implement dynamic backoff strategies, such as adjust backoff values from the http `Retry-After` headers.
///
/// # Examples
///
/// ```no_run
/// use core::time::Duration;
/// use std::error::Error;
/// use std::fmt::Display;
/// use std::fmt::Formatter;
///
/// use anyhow::Result;
/// use backon::ExponentialBuilder;
/// use backon::Retryable;
/// use reqwest::header::HeaderMap;
/// use reqwest::StatusCode;
///
/// #[derive(Debug)]
/// struct HttpError {
/// headers: HeaderMap,
/// }
///
/// impl Display for HttpError {
/// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
/// write!(f, "http error")
/// }
/// }
///
/// impl Error for HttpError {}
///
/// async fn fetch() -> Result<String> {
/// let resp = reqwest::get("https://www.rust-lang.org").await?;
/// if resp.status() != StatusCode::OK {
/// let source = HttpError {
/// headers: resp.headers().clone(),
/// };
/// return Err(anyhow::Error::new(source));
/// }
/// Ok(resp.text().await?)
/// }
///
/// #[tokio::main(flavor = "current_thread")]
/// async fn main() -> Result<()> {
/// let content = fetch
/// .retry(ExponentialBuilder::default())
/// .adjust(|err, dur| {
/// match err.downcast_ref::<HttpError>() {
/// Some(v) => {
/// if let Some(retry_after) = v.headers.get("Retry-After") {
/// // Parse the Retry-After header and adjust the backoff duration
/// let retry_after = retry_after.to_str().unwrap_or("0");
/// let retry_after = retry_after.parse::<u64>().unwrap_or(0);
/// Some(Duration::from_secs(retry_after))
/// } else {
/// dur
/// }
/// }
/// None => dur,
/// }
/// })
/// .await?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
pub fn adjust<NAF: FnMut(&E, Option<Duration>) -> Option<Duration>>(
self,
adjust: NAF,
) -> Retry<B, T, E, Fut, FutureFn, SF, RF, NF, NAF> {
Retry {
backoff: self.backoff,
retryable_fn: self.retryable_fn,
notify_fn: self.notify_fn,
sleep_fn: self.sleep_fn,
future_fn: self.future_fn,
adjust_fn: adjust,
state: self.state,
}
}
}
/// State maintains internal state of retry.
#[derive(Default)]
enum State<T, E, Fut: Future<Output = Result<T, E>>, SleepFut: Future> {
#[default]
Idle,
Polling(Fut),
Sleeping(SleepFut),
}
impl<B, T, E, Fut, FutureFn, SF, RF, NF, AF> Future
for Retry<B, T, E, Fut, FutureFn, SF, RF, NF, AF>
where
B: Backoff,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
SF: Sleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
AF: FnMut(&E, Option<Duration>) -> Option<Duration>,
{
type Output = Result<T, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// 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_fn)(&err) {
return Poll::Ready(Err(err));
}
let adjusted_backoff = (this.adjust_fn)(&err, this.backoff.next());
match adjusted_backoff {
None => return Poll::Ready(Err(err)),
Some(dur) => {
(this.notify_fn)(&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 {
extern crate alloc;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use core::time::Duration;
use tokio::sync::Mutex;
#[cfg(not(target_arch = "wasm32"))]
use tokio::test;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use super::*;
use crate::ExponentialBuilder;
async fn always_error() -> anyhow::Result<()> {
Err(anyhow::anyhow!("test_query meets error"))
}
#[test]
async fn test_retry() {
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());
}
#[test]
async fn test_retry_with_not_retryable_error() {
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);
}
#[test]
async fn test_retry_with_retryable_error() {
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);
}
#[test]
async fn test_retry_with_adjust() {
let error_times = std::sync::Mutex::new(0);
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)
// Only retry If error message is `retryable`
.when(|e| e.to_string() == "retryable")
.adjust(|_, dur| {
let mut x = error_times.lock().unwrap();
*x += 1;
dur
})
.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().unwrap(), 4);
}
#[test]
async fn test_fn_mut_when_and_notify() {
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);
}
}
#[cfg(test)]
mod custom_sleeper_tests {
extern crate alloc;
use alloc::string::ToString;
use core::future::ready;
use core::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use tokio::test;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as 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() {
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());
}
}
+420
View File
@@ -0,0 +1,420 @@
use core::future::Future;
use core::pin::Pin;
use core::task::Context;
use core::task::Poll;
use core::task::ready;
use core::time::Duration;
use crate::Backoff;
use crate::DefaultSleeper;
use crate::Sleeper;
use crate::backoff::BackoffBuilder;
use crate::sleep::MaybeSleeper;
/// `RetryableWithContext` adds retry support for functions that produce futures with results
/// and context.
///
/// This means all types implementing `FnMut(Ctx) -> impl Future<Output = (Ctx, Result<T, E>)>`
/// 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<usize> {
/// Err(anyhow!("not retryable"))
/// }
/// }
///
/// #[tokio::main(flavor = "current_thread")]
/// async fn main() -> Result<()> {
/// let mut test = Test;
///
/// // (Test, Result<usize>)
/// 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<Output = (Ctx, Result<T, E>)>,
FutureFn: FnMut(Ctx) -> Fut,
>
{
/// Generate a new retry
fn retry(self, builder: B) -> RetryWithContext<B::Backoff, T, E, Ctx, Fut, FutureFn>;
}
impl<B, T, E, Ctx, Fut, FutureFn> RetryableWithContext<B, T, E, Ctx, Fut, FutureFn> for FutureFn
where
B: BackoffBuilder,
Fut: Future<Output = (Ctx, Result<T, E>)>,
FutureFn: FnMut(Ctx) -> Fut,
{
fn retry(self, builder: B) -> RetryWithContext<B::Backoff, T, E, Ctx, Fut, FutureFn> {
RetryWithContext::new(self, builder.build())
}
}
/// Retry struct generated by [`RetryableWithContext`].
pub struct RetryWithContext<
B: Backoff,
T,
E,
Ctx,
Fut: Future<Output = (Ctx, Result<T, E>)>,
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<T, E, Ctx, Fut, SF::Sleep>,
}
impl<B, T, E, Ctx, Fut, FutureFn> RetryWithContext<B, T, E, Ctx, Fut, FutureFn>
where
B: Backoff,
Fut: Future<Output = (Ctx, Result<T, E>)>,
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<B, T, E, Ctx, Fut, FutureFn, SF, RF, NF>
RetryWithContext<B, T, E, Ctx, Fut, FutureFn, SF, RF, NF>
where
B: Backoff,
Fut: Future<Output = (Ctx, Result<T, E>)>,
FutureFn: FnMut(Ctx) -> 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`].
pub fn sleep<SN: Sleeper>(
self,
sleep_fn: SN,
) -> RetryWithContext<B, T, E, Ctx, Fut, FutureFn, SN, RF, NF> {
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<B, T, E, Ctx, Fut, FutureFn, SF, RF, NF> {
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<String> {
/// 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<RN: FnMut(&E) -> bool>(
self,
retryable: RN,
) -> RetryWithContext<B, T, E, Ctx, Fut, FutureFn, SF, RN, NF> {
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<String> {
/// 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<NN: FnMut(&E, Duration)>(
self,
notify: NN,
) -> RetryWithContext<B, T, E, Ctx, Fut, FutureFn, SF, RF, NN> {
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<T, E, Ctx, Fut: Future<Output = (Ctx, Result<T, E>)>, SleepFut: Future> {
Idle(Option<Ctx>),
Polling(Fut),
Sleeping((Option<Ctx>, SleepFut)),
}
impl<B, T, E, Ctx, Fut, FutureFn, SF, RF, NF> Future
for RetryWithContext<B, T, E, Ctx, Fut, FutureFn, SF, RF, NF>
where
B: Backoff,
Fut: Future<Output = (Ctx, Result<T, E>)>,
FutureFn: FnMut(Ctx) -> Fut,
SF: Sleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
{
type Output = (Ctx, Result<T, E>);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// 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 {
extern crate alloc;
use alloc::string::ToString;
use core::time::Duration;
use anyhow::Result;
use anyhow::anyhow;
use tokio::sync::Mutex;
#[cfg(not(target_arch = "wasm32"))]
use tokio::test;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use super::*;
use crate::ExponentialBuilder;
struct Test;
impl Test {
async fn hello(&mut self) -> Result<usize> {
Err(anyhow!("not retryable"))
}
}
#[test]
async fn test_retry_with_not_retryable_error() {
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);
}
}
+111
View File
@@ -0,0 +1,111 @@
use core::future::Future;
use core::future::Ready;
use core::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<T: Sleeper + ?Sized> MaybeSleeper for T {
type Sleep = <T as Sleeper>::Sleep;
}
/// All `Fn(Duration) -> impl Future` implements `Sleeper`.
impl<F: Fn(Duration) -> 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)]
#[allow(dead_code)]
#[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 implementation of `Sleeper` that uses `futures_timer::Delay`.
///
/// This implementation is based on
/// the [`futures-timer`](https://docs.rs/futures-timer/latest/futures_timer/) crate.
/// It is async runtime agnostic and will also work in WASM environments.
#[cfg(feature = "futures-timer-sleep")]
#[derive(Clone, Copy, Debug, Default)]
pub struct FuturesTimerSleeper;
#[cfg(feature = "futures-timer-sleep")]
impl Sleeper for FuturesTimerSleeper {
type Sleep = futures_timer::Delay;
fn sleep(&self, dur: Duration) -> Self::Sleep {
futures_timer::Delay::new(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)
}
}