backon-1.2.0/.cargo_vcs_info.json 0000644 00000000144 00000000001 0012270 0 ustar {
"git": {
"sha1": "50ce18569fe94dd3f6d285a8f82c2b0ebce2b1c6"
},
"path_in_vcs": "backon"
} backon-1.2.0/Cargo.toml 0000644 00000004255 00000000001 0010275 0 ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.70"
name = "backon"
version = "1.2.0"
build = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Make retry like a built-in feature provided by Rust."
documentation = "https://docs.rs/backon"
readme = "README.md"
license = "Apache-2.0"
repository = "https://github.com/Xuanwo/backon"
[package.metadata.docs.rs]
all-features = true
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"wasm32-unknown-unknown",
]
[lib]
name = "backon"
path = "src/lib.rs"
[dependencies.fastrand]
version = "2"
[dev-dependencies.anyhow]
version = "1"
[dev-dependencies.reqwest]
version = "0.12"
[dev-dependencies.spin]
version = "0.9.8"
[features]
default = [
"std-blocking-sleep",
"tokio-sleep",
"gloo-timers-sleep",
]
gloo-timers-sleep = [
"dep:gloo-timers",
"gloo-timers?/futures",
]
std-blocking-sleep = []
tokio-sleep = [
"dep:tokio",
"tokio?/time",
]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "1"
optional = true
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.sqlx]
version = "0.8.0"
features = [
"runtime-tokio",
"sqlite",
]
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.tokio]
version = "1"
features = [
"time",
"rt",
"macros",
"sync",
"rt-multi-thread",
]
[target.'cfg(target_arch = "wasm32")'.dependencies.gloo-timers]
version = "0.3"
optional = true
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.tokio]
version = "1"
features = [
"macros",
"rt",
"sync",
]
default-features = false
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.wasm-bindgen-test]
version = "0.3"
backon-1.2.0/Cargo.toml.orig 0000644 0000000 0000000 00000002574 10461020230 0013760 0 ustar 0000000 0000000 [package]
description = "Make retry like a built-in feature provided by Rust."
documentation = "https://docs.rs/backon"
name = "backon"
readme = "../README.md"
rust-version = "1.70"
version = "1.2.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[package.metadata.docs.rs]
all-features = true
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"wasm32-unknown-unknown",
]
[features]
default = ["std-blocking-sleep", "tokio-sleep", "gloo-timers-sleep"]
std-blocking-sleep = []
gloo-timers-sleep = ["dep:gloo-timers", "gloo-timers?/futures"]
tokio-sleep = ["dep:tokio", "tokio?/time"]
[dependencies]
fastrand = "2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
gloo-timers = { version = "0.3", optional = true }
[dev-dependencies]
anyhow = "1"
reqwest = "0.12"
spin = "0.9.8"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
tokio = { version = "1", features = [
"macros",
"rt",
"sync",
], default-features = false }
wasm-bindgen-test = "0.3"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
sqlx = { version = "0.8.0", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1", features = [
"time",
"rt",
"macros",
"sync",
"rt-multi-thread",
] }
backon-1.2.0/README.md 0000644 0000000 0000000 00000005761 10461020230 0012351 0 ustar 0000000 0000000 # BackON [![Build Status]][actions] [![Latest Version]][crates.io] [](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
Make **retry** like a built-in feature provided by Rust.
- **Simple**: Just like a built-in feature: `your_fn.retry(ExponentialBuilder::default()).await`.
- **Flexible**: Supports both blocking and async functions.
- **Powerful**: Allows control over retry behavior such as [`when`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.when) and [`notify`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.notify).
- **Customizable**: Supports custom retry strategies like [exponential](https://docs.rs/backon/latest/backon/struct.ExponentialBuilder.html), [constant](https://docs.rs/backon/latest/backon/struct.ConstantBuilder.html), etc.
---
## Quick Start
### Retry an async function.
```rust
use anyhow::Result;
use backon::ExponentialBuilder;
use backon::Retryable;
async fn fetch() -> Result {
Ok("hello, world!".to_string())
}
#[tokio::main]
async fn main() -> Result<()> {
let content = fetch
// Retry with exponential backoff
.retry(ExponentialBuilder::default())
// Sleep implementation, required if no feature has been enabled
.sleep(tokio::time::sleep)
// When to retry
.when(|e| e.to_string() == "EOF")
// Notify when retrying
.notify(|err: &anyhow::Error, dur: Duration| {
println!("retrying {:?} after {:?}", err, dur);
})
.await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
### Retry a blocking function.
```rust
use anyhow::Result;
use backon::BlockingRetryable;
use backon::ExponentialBuilder;
fn fetch() -> Result {
Ok("hello, world!".to_string())
}
fn main() -> Result<()> {
let content = fetch
// Retry with exponential backoff
.retry(ExponentialBuilder::default())
// When to retry
.when(|e| e.to_string() == "EOF")
// Notify when retrying
.notify(|err: &anyhow::Error, dur: Duration| {
println!("retrying {:?} after {:?}", err, dur);
})
.call()?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
## Contributing
Check out the [CONTRIBUTING.md](./CONTRIBUTING.md) guide for more details on getting started with contributing to this
project.
## Getting help
Submit [issues](https://github.com/Xuanwo/backon/issues/new/choose) for bug report or asking questions
in [discussion](https://github.com/Xuanwo/backon/discussions/new?category=q-a).
## License
Licensed under Apache License, Version 2.0.
backon-1.2.0/src/backoff/api.rs 0000644 0000000 0000000 00000001373 10461020230 0014366 0 ustar 0000000 0000000 use core::fmt::Debug;
use core::time::Duration;
/// BackoffBuilder is utilized to construct a new backoff.
pub trait BackoffBuilder: Debug + Send + Sync + Unpin {
/// The associated backoff returned by this builder.
type Backoff: Backoff;
/// Construct a new backoff using the builder.
fn build(self) -> Self::Backoff;
}
/// Backoff is an [`Iterator`] that returns [`Duration`].
///
/// - `Some(Duration)` indicates the caller should `sleep(Duration)` and retry the request.
/// - `None` indicates the limits have been reached, and the caller should return the current error instead.
pub trait Backoff: Iterator- + Send + Sync + Unpin {}
impl Backoff for T where T: Iterator
- + Debug + Send + Sync + Unpin {}
backon-1.2.0/src/backoff/constant.rs 0000644 0000000 0000000 00000010252 10461020230 0015442 0 ustar 0000000 0000000 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 {
/// Ok(reqwest::get("https://www.rust-lang.org")
/// .await?
/// .text()
/// .await?)
/// }
///
/// #[tokio::main(flavor = "current_thread")]
/// async fn main() -> Result<()> {
/// let content = fetch.retry(ConstantBuilder::default()).await?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
#[derive(Debug, Clone, Copy)]
pub struct ConstantBuilder {
delay: Duration,
max_times: Option,
jitter: bool,
}
impl Default for ConstantBuilder {
fn default() -> Self {
Self {
delay: Duration::from_secs(1),
max_times: Some(3),
jitter: false,
}
}
}
impl ConstantBuilder {
/// Set the delay for the backoff.
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
/// Set the maximum duration for the backoff.
pub fn with_max_times(mut self, max_times: usize) -> Self {
self.max_times = Some(max_times);
self
}
/// Set jitter for the backoff.
///
/// Jitter is a random value added to the delay to prevent a thundering herd problem.
pub fn with_jitter(mut self) -> Self {
self.jitter = true;
self
}
}
impl BackoffBuilder for ConstantBuilder {
type Backoff = ConstantBackoff;
fn build(self) -> Self::Backoff {
ConstantBackoff {
delay: self.delay,
max_times: self.max_times,
attempts: 0,
jitter: self.jitter,
}
}
}
/// ConstantBackoff offers a consistent delay with a limited number of retries.
///
/// This backoff strategy is constructed by [`ConstantBuilder`].
#[doc(hidden)]
#[derive(Debug)]
pub struct ConstantBackoff {
delay: Duration,
max_times: Option,
attempts: usize,
jitter: bool,
}
impl Iterator for ConstantBackoff {
type Item = Duration;
fn next(&mut self) -> Option {
let delay = || match self.jitter {
true => self.delay + self.delay.mul_f32(fastrand::f32()),
false => self.delay,
};
match self.max_times {
None => Some(delay()),
Some(max_times) => {
if self.attempts >= max_times {
None
} else {
self.attempts += 1;
Some(delay())
}
}
}
}
}
#[cfg(test)]
mod tests {
use core::time::Duration;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use crate::BackoffBuilder;
use crate::ConstantBuilder;
#[test]
fn test_constant_default() {
let mut exp = ConstantBuilder::default().build();
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_constant_with_delay() {
let mut exp = ConstantBuilder::default()
.with_delay(Duration::from_secs(2))
.build();
assert_eq!(Some(Duration::from_secs(2)), exp.next());
assert_eq!(Some(Duration::from_secs(2)), exp.next());
assert_eq!(Some(Duration::from_secs(2)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_constant_with_times() {
let mut exp = ConstantBuilder::default().with_max_times(1).build();
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_constant_with_jitter() {
let mut it = ConstantBuilder::default().with_jitter().build();
let dur = it.next().unwrap();
fastrand::seed(7);
assert!(dur > Duration::from_secs(1));
}
}
backon-1.2.0/src/backoff/exponential.rs 0000644 0000000 0000000 00000022476 10461020230 0016152 0 ustar 0000000 0000000 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 {
/// Ok(reqwest::get("https://www.rust-lang.org")
/// .await?
/// .text()
/// .await?)
/// }
///
/// #[tokio::main(flavor = "current_thread")]
/// async fn main() -> Result<()> {
/// let content = fetch.retry(ExponentialBuilder::default()).await?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
#[derive(Debug, Clone, Copy)]
pub struct ExponentialBuilder {
jitter: bool,
factor: f32,
min_delay: Duration,
max_delay: Option,
max_times: Option,
}
impl Default for ExponentialBuilder {
fn default() -> Self {
Self {
jitter: false,
factor: 2.0,
min_delay: Duration::from_secs(1),
max_delay: Some(Duration::from_secs(60)),
max_times: Some(3),
}
}
}
impl ExponentialBuilder {
/// Set the jitter for the backoff.
///
/// When jitter is enabled, [`ExponentialBackoff`] will add a random jitter within `(0, min_delay)`
/// to the current delay.
pub fn with_jitter(mut self) -> Self {
self.jitter = true;
self
}
/// Set the factor for the backoff.
///
/// # Panics
///
/// This function will panic if the input factor is less than `1.0`.
pub fn with_factor(mut self, factor: f32) -> Self {
debug_assert!(factor >= 1.0, "invalid factor that lower than 1");
self.factor = factor;
self
}
/// Set the minimum delay for the backoff.
pub fn with_min_delay(mut self, min_delay: Duration) -> Self {
self.min_delay = min_delay;
self
}
/// Set the maximum delay for the backoff.
///
/// The delay will not increase if the current delay exceeds the maximum delay.
pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = Some(max_delay);
self
}
/// Set the maximum number of attempts for the current backoff.
///
/// The backoff will stop if the maximum number of attempts is reached.
pub fn with_max_times(mut self, max_times: usize) -> Self {
self.max_times = Some(max_times);
self
}
}
impl BackoffBuilder for ExponentialBuilder {
type Backoff = ExponentialBackoff;
fn build(self) -> Self::Backoff {
ExponentialBackoff {
jitter: self.jitter,
factor: self.factor,
min_delay: self.min_delay,
max_delay: self.max_delay,
max_times: self.max_times,
current_delay: None,
attempts: 0,
}
}
}
/// ExponentialBackoff provides a delay with exponential retries.
///
/// This backoff strategy is constructed by [`ExponentialBuilder`].
#[doc(hidden)]
#[derive(Debug)]
pub struct ExponentialBackoff {
jitter: bool,
factor: f32,
min_delay: Duration,
max_delay: Option,
max_times: Option,
current_delay: Option,
attempts: usize,
}
impl Iterator for ExponentialBackoff {
type Item = Duration;
fn next(&mut self) -> Option {
if self.attempts >= self.max_times.unwrap_or(usize::MAX) {
return None;
}
self.attempts += 1;
let mut tmp_cur = match self.current_delay {
None => {
// If current_delay is None, it's must be the first time to retry.
self.current_delay = Some(self.min_delay);
self.min_delay
}
Some(mut cur) => {
// If current delay larger than max delay, we should stop increment anymore.
if let Some(max_delay) = self.max_delay {
if cur < max_delay {
cur = saturating_mul(cur, self.factor);
}
if cur > max_delay {
cur = max_delay;
}
} else {
cur = saturating_mul(cur, self.factor);
}
self.current_delay = Some(cur);
cur
}
};
// If jitter is enabled, add random jitter based on min delay.
if self.jitter {
tmp_cur = tmp_cur.saturating_add(self.min_delay.mul_f32(fastrand::f32()));
}
Some(tmp_cur)
}
}
#[inline]
pub(crate) fn saturating_mul(d: Duration, rhs: f32) -> Duration {
match Duration::try_from_secs_f32(rhs * d.as_secs_f32()) {
Ok(v) => v,
Err(_) => Duration::MAX,
}
}
#[cfg(test)]
mod tests {
use core::time::Duration;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use crate::BackoffBuilder;
use crate::ExponentialBuilder;
#[test]
fn test_exponential_default() {
let mut exp = ExponentialBuilder::default().build();
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(Some(Duration::from_secs(2)), exp.next());
assert_eq!(Some(Duration::from_secs(4)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_factor() {
let mut exp = ExponentialBuilder::default().with_factor(1.5).build();
assert_eq!(Some(Duration::from_secs_f32(1.0)), exp.next());
assert_eq!(Some(Duration::from_secs_f32(1.5)), exp.next());
assert_eq!(Some(Duration::from_secs_f32(2.25)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_jitter() {
let mut exp = ExponentialBuilder::default().with_jitter().build();
let v = exp.next().expect("value must valid");
assert!(v >= Duration::from_secs(1), "current: {v:?}");
assert!(v < Duration::from_secs(2), "current: {v:?}");
let v = exp.next().expect("value must valid");
assert!(v >= Duration::from_secs(2), "current: {v:?}");
assert!(v < Duration::from_secs(4), "current: {v:?}");
let v = exp.next().expect("value must valid");
assert!(v >= Duration::from_secs(4), "current: {v:?}");
assert!(v < Duration::from_secs(8), "current: {v:?}");
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_min_delay() {
let mut exp = ExponentialBuilder::default()
.with_min_delay(Duration::from_millis(500))
.build();
assert_eq!(Some(Duration::from_millis(500)), exp.next());
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(Some(Duration::from_secs(2)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_max_delay_with_default() {
let mut exp = ExponentialBuilder::default()
.with_max_delay(Duration::from_secs(2))
.build();
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(Some(Duration::from_secs(2)), exp.next());
assert_eq!(Some(Duration::from_secs(2)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_max_delay_without_default_1() {
let mut exp = ExponentialBuilder {
jitter: false,
factor: 10_000_000_000_f32,
min_delay: Duration::from_secs(1),
max_delay: None,
max_times: None,
}
.build();
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next());
assert_eq!(Some(Duration::MAX), exp.next());
assert_eq!(Some(Duration::MAX), exp.next());
}
#[test]
fn test_exponential_max_delay_without_default_2() {
let mut exp = ExponentialBuilder {
jitter: true,
factor: 10_000_000_000_f32,
min_delay: Duration::from_secs(10_000_000_000),
max_delay: None,
max_times: Some(2),
}
.build();
let v = exp.next().expect("value must valid");
assert!(v >= Duration::from_secs(10_000_000_000), "current: {v:?}");
assert!(v < Duration::from_secs(20_000_000_000), "current: {v:?}");
assert_eq!(Some(Duration::MAX), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_max_delay_without_default_3() {
let mut exp = ExponentialBuilder {
jitter: false,
factor: 10_000_000_000_f32,
min_delay: Duration::from_secs(10_000_000_000),
max_delay: Some(Duration::from_secs(60_000_000_000)),
max_times: Some(3),
}
.build();
assert_eq!(Some(Duration::from_secs(10_000_000_000)), exp.next());
assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next());
assert_eq!(Some(Duration::from_secs(60_000_000_000)), exp.next());
assert_eq!(None, exp.next());
}
#[test]
fn test_exponential_max_times() {
let mut exp = ExponentialBuilder::default().with_max_times(1).build();
assert_eq!(Some(Duration::from_secs(1)), exp.next());
assert_eq!(None, exp.next());
}
}
backon-1.2.0/src/backoff/fibonacci.rs 0000644 0000000 0000000 00000016063 10461020230 0015534 0 ustar 0000000 0000000 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 {
/// Ok(reqwest::get("https://www.rust-lang.org")
/// .await?
/// .text()
/// .await?)
/// }
///
/// #[tokio::main(flavor = "current_thread")]
/// async fn main() -> Result<()> {
/// let content = fetch.retry(FibonacciBuilder::default()).await?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
#[derive(Debug, Clone, Copy)]
pub struct FibonacciBuilder {
jitter: bool,
min_delay: Duration,
max_delay: Option,
max_times: Option,
}
impl Default for FibonacciBuilder {
fn default() -> Self {
Self {
jitter: false,
min_delay: Duration::from_secs(1),
max_delay: Some(Duration::from_secs(60)),
max_times: Some(3),
}
}
}
impl FibonacciBuilder {
/// Set the jitter for the backoff.
///
/// When jitter is enabled, FibonacciBackoff will add a random jitter between `(0, min_delay)` to the delay.
pub fn with_jitter(mut self) -> Self {
self.jitter = true;
self
}
/// Set the minimum delay for the backoff.
pub fn with_min_delay(mut self, min_delay: Duration) -> Self {
self.min_delay = min_delay;
self
}
/// Set the maximum delay for the current backoff.
///
/// The delay will not increase if the current delay exceeds the maximum delay.
pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = Some(max_delay);
self
}
/// Set the maximum number of attempts for the current backoff.
///
/// The backoff will stop if the maximum number of attempts is reached.
pub fn with_max_times(mut self, max_times: usize) -> Self {
self.max_times = Some(max_times);
self
}
}
impl BackoffBuilder for FibonacciBuilder {
type Backoff = FibonacciBackoff;
fn build(self) -> Self::Backoff {
FibonacciBackoff {
jitter: self.jitter,
min_delay: self.min_delay,
max_delay: self.max_delay,
max_times: self.max_times,
previous_delay: None,
current_delay: None,
attempts: 0,
}
}
}
/// FibonacciBackoff offers a delay with Fibonacci-based retries.
///
/// This backoff strategy is constructed by [`FibonacciBuilder`].
#[doc(hidden)]
#[derive(Debug)]
pub struct FibonacciBackoff {
jitter: bool,
min_delay: Duration,
max_delay: Option,
max_times: Option,
previous_delay: Option,
current_delay: Option,
attempts: usize,
}
impl Iterator for FibonacciBackoff {
type Item = Duration;
fn next(&mut self) -> Option {
if self.attempts >= self.max_times.unwrap_or(usize::MAX) {
return None;
}
self.attempts += 1;
match self.current_delay {
None => {
// If current_delay is None, it's must be the first time to retry.
let mut next = self.min_delay;
self.current_delay = Some(next);
// If jitter is enabled, add random jitter based on min delay.
if self.jitter {
next += self.min_delay.mul_f32(fastrand::f32());
}
Some(next)
}
Some(cur) => {
let mut next = cur;
// If current delay larger than max delay, we should stop increment anymore.
if next < self.max_delay.unwrap_or(Duration::MAX) {
if let Some(prev) = self.previous_delay {
next += prev;
self.current_delay = Some(next);
}
self.previous_delay = Some(cur);
}
// If jitter is enabled, add random jitter based on min delay.
if self.jitter {
next += self.min_delay.mul_f32(fastrand::f32());
}
Some(next)
}
}
}
}
#[cfg(test)]
mod tests {
use core::time::Duration;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use crate::BackoffBuilder;
use crate::FibonacciBuilder;
#[test]
fn test_fibonacci_default() {
let mut fib = FibonacciBuilder::default().build();
assert_eq!(Some(Duration::from_secs(1)), fib.next());
assert_eq!(Some(Duration::from_secs(1)), fib.next());
assert_eq!(Some(Duration::from_secs(2)), fib.next());
assert_eq!(None, fib.next());
}
#[test]
fn test_fibonacci_jitter() {
let mut fib = FibonacciBuilder::default().with_jitter().build();
let v = fib.next().expect("value must valid");
assert!(v >= Duration::from_secs(1), "current: {v:?}");
assert!(v < Duration::from_secs(2), "current: {v:?}");
let v = fib.next().expect("value must valid");
assert!(v >= Duration::from_secs(1), "current: {v:?}");
assert!(v < Duration::from_secs(2), "current: {v:?}");
let v = fib.next().expect("value must valid");
assert!(v >= Duration::from_secs(2), "current: {v:?}");
assert!(v < Duration::from_secs(3), "current: {v:?}");
assert_eq!(None, fib.next());
}
#[test]
fn test_fibonacci_min_delay() {
let mut fib = FibonacciBuilder::default()
.with_min_delay(Duration::from_millis(500))
.build();
assert_eq!(Some(Duration::from_millis(500)), fib.next());
assert_eq!(Some(Duration::from_millis(500)), fib.next());
assert_eq!(Some(Duration::from_secs(1)), fib.next());
assert_eq!(None, fib.next());
}
#[test]
fn test_fibonacci_max_delay() {
let mut fib = FibonacciBuilder::default()
.with_max_times(4)
.with_max_delay(Duration::from_secs(2))
.build();
assert_eq!(Some(Duration::from_secs(1)), fib.next());
assert_eq!(Some(Duration::from_secs(1)), fib.next());
assert_eq!(Some(Duration::from_secs(2)), fib.next());
assert_eq!(Some(Duration::from_secs(2)), fib.next());
assert_eq!(None, fib.next());
}
#[test]
fn test_fibonacci_max_times() {
let mut fib = FibonacciBuilder::default().with_max_times(6).build();
assert_eq!(Some(Duration::from_secs(1)), fib.next());
assert_eq!(Some(Duration::from_secs(1)), fib.next());
assert_eq!(Some(Duration::from_secs(2)), fib.next());
assert_eq!(Some(Duration::from_secs(3)), fib.next());
assert_eq!(Some(Duration::from_secs(5)), fib.next());
assert_eq!(Some(Duration::from_secs(8)), fib.next());
assert_eq!(None, fib.next());
}
}
backon-1.2.0/src/backoff/mod.rs 0000644 0000000 0000000 00000000454 10461020230 0014373 0 ustar 0000000 0000000 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;
backon-1.2.0/src/blocking_retry.rs 0000644 0000000 0000000 00000023335 10461020230 0015241 0 ustar 0000000 0000000 use core::time::Duration;
use crate::backoff::BackoffBuilder;
use crate::blocking_sleep::MaybeBlockingSleeper;
use crate::{Backoff, BlockingSleeper, DefaultBlockingSleeper};
/// BlockingRetryable adds retry support for blocking functions.
///
/// For example:
///
/// - Functions without extra args:
///
/// ```ignore
/// fn fetch() -> Result {
/// Ok("hello, world!".to_string())
/// }
/// ```
///
/// - Closures
///
/// ```ignore
/// || {
/// Ok("hello, world!".to_string())
/// }
/// ```
///
/// # Example
///
/// ```no_run
/// use anyhow::Result;
/// use backon::BlockingRetryable;
/// use backon::ExponentialBuilder;
///
/// fn fetch() -> Result {
/// Ok("hello, world!".to_string())
/// }
///
/// fn main() -> Result<()> {
/// let content = fetch.retry(ExponentialBuilder::default()).call()?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
pub trait BlockingRetryable Result> {
/// Generate a new retry.
fn retry(self, builder: B) -> BlockingRetry;
}
impl BlockingRetryable for F
where
B: BackoffBuilder,
F: FnMut() -> Result,
{
fn retry(self, builder: B) -> BlockingRetry {
BlockingRetry::new(self, builder.build())
}
}
/// Retry structure generated by [`BlockingRetryable`].
pub struct BlockingRetry<
B: Backoff,
T,
E,
F: FnMut() -> Result,
SF: MaybeBlockingSleeper = DefaultBlockingSleeper,
RF = fn(&E) -> bool,
NF = fn(&E, Duration),
> {
backoff: B,
retryable: RF,
notify: NF,
f: F,
sleep_fn: SF,
}
impl BlockingRetry
where
B: Backoff,
F: FnMut() -> Result,
{
/// Create a new retry.
fn new(f: F, backoff: B) -> Self {
BlockingRetry {
backoff,
retryable: |_: &E| true,
notify: |_: &E, _: Duration| {},
sleep_fn: DefaultBlockingSleeper::default(),
f,
}
}
}
impl BlockingRetry
where
B: Backoff,
F: FnMut() -> Result,
SF: MaybeBlockingSleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
{
/// Set the sleeper for retrying.
///
/// The sleeper should implement the [`BlockingSleeper`] trait. The simplest way is to use a closure like `Fn(Duration)`.
///
/// If not specified, we use the [`DefaultBlockingSleeper`].
///
/// # Examples
///
/// ```no_run
/// use anyhow::Result;
/// use backon::BlockingRetryable;
/// use backon::ExponentialBuilder;
///
/// fn fetch() -> Result {
/// Ok("hello, world!".to_string())
/// }
///
/// fn main() -> Result<()> {
/// let retry = fetch
/// .retry(ExponentialBuilder::default())
/// .sleep(std::thread::sleep);
/// let content = retry.call()?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
pub fn sleep(self, sleep_fn: SN) -> BlockingRetry {
BlockingRetry {
backoff: self.backoff,
retryable: self.retryable,
notify: self.notify,
f: self.f,
sleep_fn,
}
}
/// Set the conditions for retrying.
///
/// If not specified, all errors are considered retryable.
///
/// # Examples
///
/// ```no_run
/// use anyhow::Result;
/// use backon::BlockingRetryable;
/// use backon::ExponentialBuilder;
///
/// fn fetch() -> Result {
/// Ok("hello, world!".to_string())
/// }
///
/// fn main() -> Result<()> {
/// let retry = fetch
/// .retry(ExponentialBuilder::default())
/// .when(|e| e.to_string() == "EOF");
/// let content = retry.call()?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
pub fn when bool>(
self,
retryable: RN,
) -> BlockingRetry {
BlockingRetry {
backoff: self.backoff,
retryable,
notify: self.notify,
f: self.f,
sleep_fn: self.sleep_fn,
}
}
/// Set to notify for all retry attempts.
///
/// When a retry happens, the input function will be invoked with the error and the sleep duration before pausing.
///
/// If not specified, this operation does nothing.
///
/// # Examples
///
/// ```no_run
/// use core::time::Duration;
///
/// use anyhow::Result;
/// use backon::BlockingRetryable;
/// use backon::ExponentialBuilder;
///
/// fn fetch() -> Result {
/// Ok("hello, world!".to_string())
/// }
///
/// fn main() -> Result<()> {
/// let retry = fetch.retry(ExponentialBuilder::default()).notify(
/// |err: &anyhow::Error, dur: Duration| {
/// println!("retrying error {:?} with sleeping {:?}", err, dur);
/// },
/// );
/// let content = retry.call()?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
pub fn notify(
self,
notify: NN,
) -> BlockingRetry {
BlockingRetry {
backoff: self.backoff,
retryable: self.retryable,
notify,
f: self.f,
sleep_fn: self.sleep_fn,
}
}
}
impl BlockingRetry
where
B: Backoff,
F: FnMut() -> Result,
SF: BlockingSleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
{
/// Call the retried function.
///
/// TODO: implement [`FnOnce`] after it stable.
pub fn call(mut self) -> Result {
loop {
let result = (self.f)();
match result {
Ok(v) => return Ok(v),
Err(err) => {
if !(self.retryable)(&err) {
return Err(err);
}
match self.backoff.next() {
None => return Err(err),
Some(dur) => {
(self.notify)(&err, dur);
self.sleep_fn.sleep(dur);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use core::time::Duration;
use spin::Mutex;
use super::*;
use crate::ExponentialBuilder;
fn always_error() -> anyhow::Result<()> {
Err(anyhow::anyhow!("test_query meets error"))
}
#[test]
fn test_retry() -> anyhow::Result<()> {
let result = always_error
.retry(ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)))
.call();
assert!(result.is_err());
assert_eq!("test_query meets error", result.unwrap_err().to_string());
Ok(())
}
#[test]
fn test_retry_with_not_retryable_error() -> anyhow::Result<()> {
let error_times = Mutex::new(0);
let f = || {
let mut x = error_times.lock();
*x += 1;
Err::<(), anyhow::Error>(anyhow::anyhow!("not retryable"))
};
let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1));
let result = f
.retry(backoff)
// Only retry If error message is `retryable`
.when(|e| e.to_string() == "retryable")
.call();
assert!(result.is_err());
assert_eq!("not retryable", result.unwrap_err().to_string());
// `f` always returns error "not retryable", so it should be executed
// only once.
assert_eq!(*error_times.lock(), 1);
Ok(())
}
#[test]
fn test_retry_with_retryable_error() -> anyhow::Result<()> {
let error_times = Mutex::new(0);
let f = || {
// println!("I have been called!");
let mut x = error_times.lock();
*x += 1;
Err::<(), anyhow::Error>(anyhow::anyhow!("retryable"))
};
let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1));
let result = f
.retry(backoff)
// Only retry If error message is `retryable`
.when(|e| e.to_string() == "retryable")
.call();
assert!(result.is_err());
assert_eq!("retryable", result.unwrap_err().to_string());
// `f` always returns error "retryable", so it should be executed
// 4 times (retry 3 times).
assert_eq!(*error_times.lock(), 4);
Ok(())
}
#[test]
fn test_fn_mut_when_and_notify() -> anyhow::Result<()> {
let mut calls_retryable: Vec<()> = vec![];
let mut calls_notify: Vec<()> = vec![];
let f = || Err::<(), anyhow::Error>(anyhow::anyhow!("retryable"));
let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1));
let result = f
.retry(backoff)
.when(|_| {
calls_retryable.push(());
true
})
.notify(|_, _| {
calls_notify.push(());
})
.call();
assert!(result.is_err());
assert_eq!("retryable", result.unwrap_err().to_string());
// `f` always returns error "retryable", so it should be executed
// 4 times (retry 3 times).
assert_eq!(calls_retryable.len(), 4);
assert_eq!(calls_notify.len(), 3);
Ok(())
}
}
backon-1.2.0/src/blocking_retry_with_context.rs 0000644 0000000 0000000 00000014522 10461020230 0020036 0 ustar 0000000 0000000 use core::time::Duration;
use crate::backoff::BackoffBuilder;
use crate::blocking_sleep::MaybeBlockingSleeper;
use crate::{Backoff, BlockingSleeper, DefaultBlockingSleeper};
/// BlockingRetryableWithContext adds retry support for blocking functions.
pub trait BlockingRetryableWithContext<
B: BackoffBuilder,
T,
E,
Ctx,
F: FnMut(Ctx) -> (Ctx, Result),
>
{
/// Generate a new retry
fn retry(self, builder: B) -> BlockingRetryWithContext;
}
impl BlockingRetryableWithContext for F
where
B: BackoffBuilder,
F: FnMut(Ctx) -> (Ctx, Result),
{
fn retry(self, builder: B) -> BlockingRetryWithContext {
BlockingRetryWithContext::new(self, builder.build())
}
}
/// Retry structure generated by [`BlockingRetryableWithContext`].
pub struct BlockingRetryWithContext<
B: Backoff,
T,
E,
Ctx,
F: FnMut(Ctx) -> (Ctx, Result),
SF: MaybeBlockingSleeper = DefaultBlockingSleeper,
RF = fn(&E) -> bool,
NF = fn(&E, Duration),
> {
backoff: B,
retryable: RF,
notify: NF,
f: F,
sleep_fn: SF,
ctx: Option,
}
impl BlockingRetryWithContext
where
B: Backoff,
F: FnMut(Ctx) -> (Ctx, Result),
{
/// Create a new retry.
fn new(f: F, backoff: B) -> Self {
BlockingRetryWithContext {
backoff,
retryable: |_: &E| true,
notify: |_: &E, _: Duration| {},
sleep_fn: DefaultBlockingSleeper::default(),
f,
ctx: None,
}
}
}
impl BlockingRetryWithContext
where
B: Backoff,
F: FnMut(Ctx) -> (Ctx, Result),
SF: MaybeBlockingSleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
{
/// Set the context for retrying.
///
/// Context is used to capture ownership manually to prevent lifetime issues.
pub fn context(self, context: Ctx) -> BlockingRetryWithContext {
BlockingRetryWithContext {
backoff: self.backoff,
retryable: self.retryable,
notify: self.notify,
f: self.f,
sleep_fn: self.sleep_fn,
ctx: Some(context),
}
}
/// Set the sleeper for retrying.
///
/// The sleeper should implement the [`BlockingSleeper`] trait. The simplest way is to use a closure like `Fn(Duration)`.
///
/// If not specified, we use the [`DefaultBlockingSleeper`].
pub fn sleep(
self,
sleep_fn: SN,
) -> BlockingRetryWithContext {
BlockingRetryWithContext {
backoff: self.backoff,
retryable: self.retryable,
notify: self.notify,
f: self.f,
sleep_fn,
ctx: self.ctx,
}
}
/// Set the conditions for retrying.
///
/// If not specified, all errors are considered retryable.
pub fn when bool>(
self,
retryable: RN,
) -> BlockingRetryWithContext {
BlockingRetryWithContext {
backoff: self.backoff,
retryable,
notify: self.notify,
f: self.f,
sleep_fn: self.sleep_fn,
ctx: self.ctx,
}
}
/// Set to notify for all retry attempts.
///
/// When a retry happens, the input function will be invoked with the error and the sleep duration before pausing.
///
/// If not specified, this operation does nothing.
pub fn notify(
self,
notify: NN,
) -> BlockingRetryWithContext {
BlockingRetryWithContext {
backoff: self.backoff,
retryable: self.retryable,
notify,
f: self.f,
sleep_fn: self.sleep_fn,
ctx: self.ctx,
}
}
}
impl BlockingRetryWithContext
where
B: Backoff,
F: FnMut(Ctx) -> (Ctx, Result),
SF: BlockingSleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
{
/// Call the retried function.
///
/// TODO: implement [`FnOnce`] after it stable.
pub fn call(mut self) -> (Ctx, Result) {
let mut ctx = self.ctx.take().expect("context must be valid");
loop {
let (xctx, result) = (self.f)(ctx);
// return ctx ownership back
ctx = xctx;
match result {
Ok(v) => return (ctx, Ok(v)),
Err(err) => {
if !(self.retryable)(&err) {
return (ctx, Err(err));
}
match self.backoff.next() {
None => return (ctx, Err(err)),
Some(dur) => {
(self.notify)(&err, dur);
self.sleep_fn.sleep(dur);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ExponentialBuilder;
use alloc::string::ToString;
use anyhow::anyhow;
use anyhow::Result;
use core::time::Duration;
use spin::Mutex;
struct Test;
impl Test {
fn hello(&mut self) -> Result {
Err(anyhow!("not retryable"))
}
}
#[test]
fn test_retry_with_not_retryable_error() -> Result<()> {
let error_times = Mutex::new(0);
let test = Test;
let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_millis(1));
let (_, result) = {
|mut v: Test| {
let mut x = error_times.lock();
*x += 1;
let res = v.hello();
(v, res)
}
}
.retry(backoff)
.context(test)
// Only retry If error message is `retryable`
.when(|e| e.to_string() == "retryable")
.call();
assert!(result.is_err());
assert_eq!("not retryable", result.unwrap_err().to_string());
// `f` always returns error "not retryable", so it should be executed
// only once.
assert_eq!(*error_times.lock(), 1);
Ok(())
}
}
backon-1.2.0/src/blocking_sleep.rs 0000644 0000000 0000000 00000004342 10461020230 0015201 0 ustar 0000000 0000000 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 MaybeBlockingSleeper for T {}
/// All `Fn(Duration)` implements `Sleeper`.
impl BlockingSleeper for F {
fn sleep(&self, dur: Duration) {
self(dur)
}
}
/// The default implementation of `Sleeper` when no features are enabled.
///
/// It will fail to compile if a containing [`Retry`][crate::Retry] is `.await`ed without calling [`Retry::sleep`][crate::Retry::sleep] to provide a valid sleeper.
#[cfg(not(feature = "std-blocking-sleep"))]
pub type DefaultBlockingSleeper = PleaseEnableAFeatureOrProvideACustomSleeper;
/// The default implementation of `Sleeper` while feature `std-blocking-sleep` enabled.
///
/// it uses [`std::thread::sleep`].
#[cfg(feature = "std-blocking-sleep")]
pub type DefaultBlockingSleeper = StdSleeper;
/// A placeholder type that does not implement [`Sleeper`] and will therefore fail to compile if used as one.
///
/// Users should enable a feature of this crate that provides a valid [`Sleeper`] implementation when this type appears in compilation errors. Alternatively, a custom [`Sleeper`] implementation should be provided where necessary, such as in [`crate::Retry::sleeper`].
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default)]
pub struct PleaseEnableAFeatureOrProvideACustomSleeper;
/// Implement `MaybeSleeper` but not `Sleeper`.
impl MaybeBlockingSleeper for PleaseEnableAFeatureOrProvideACustomSleeper {}
/// The implementation of `StdSleeper` uses [`std::thread::sleep`].
#[cfg(feature = "std-blocking-sleep")]
#[derive(Clone, Copy, Debug, Default)]
pub struct StdSleeper;
#[cfg(feature = "std-blocking-sleep")]
impl BlockingSleeper for StdSleeper {
fn sleep(&self, dur: Duration) {
std::thread::sleep(dur)
}
}
backon-1.2.0/src/docs/examples/basic.md 0000644 0000000 0000000 00000000556 10461020230 0016027 0 ustar 0000000 0000000 Retry an async function.
```rust
use backon::ExponentialBuilder;
use backon::Retryable;
use anyhow::Result;
async fn fetch() -> Result {
Ok("Hello, World!".to_string())
}
#[tokio::main]
async fn main() -> Result<()> {
let content = fetch.retry(ExponentialBuilder::default()).await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
backon-1.2.0/src/docs/examples/closure.md 0000644 0000000 0000000 00000000562 10461020230 0016417 0 ustar 0000000 0000000 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::(var);
let result = f.retry(backon::ExponentialBuilder::default()).call()?;
println!("var = {result}");
Ok(())
}
```
backon-1.2.0/src/docs/examples/inside_mut_self.md 0000644 0000000 0000000 00000001106 10461020230 0020107 0 ustar 0000000 0000000 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 {
Ok(reqwest::get(url).await?.text().await?)
}
async fn run(&mut self) -> Result {
let content = (|| async { self.fetch("https://www.rust-lang.org").await })
.retry(ExponentialBuilder::default())
.when(|e| e.to_string() == "retryable")
.await?;
Ok(content)
}
}
```
backon-1.2.0/src/docs/examples/mod.rs 0000644 0000000 0000000 00000001020 10461020230 0015534 0 ustar 0000000 0000000 //! Examples of using backon.
#[doc = include_str!("basic.md")]
pub mod basic {}
#[doc = include_str!("closure.md")]
pub mod closure {}
#[doc = include_str!("inside_mut_self.md")]
pub mod inside_mut_self {}
#[doc = include_str!("sqlx.md")]
pub mod sqlx {}
#[doc = include_str!("with_args.md")]
pub mod with_args {}
#[doc = include_str!("with_mut_self.md")]
pub mod with_mut_self {}
#[doc = include_str!("with_self.md")]
pub mod with_self {}
#[doc = include_str!("with_specific_error.md")]
pub mod with_specific_error {}
backon-1.2.0/src/docs/examples/sqlx.md 0000644 0000000 0000000 00000000747 10461020230 0015737 0 ustar 0000000 0000000 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(())
}
```
backon-1.2.0/src/docs/examples/with_args.md 0000644 0000000 0000000 00000001260 10461020230 0016726 0 ustar 0000000 0000000 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 {
Ok(reqwest::get(url).await?.text().await?)
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let content = (|| async { fetch("https://www.rust-lang.org").await })
.retry(ExponentialBuilder::default())
.when(|e| e.to_string() == "retryable")
.await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
backon-1.2.0/src/docs/examples/with_mut_self.md 0000644 0000000 0000000 00000001704 10461020230 0017613 0 ustar 0000000 0000000 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 {
Ok(reqwest::get(url).await?.text().await?)
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let test = Test;
let (_, result) = (|mut v: Test| async {
let res = v.fetch("https://www.rust-lang.org").await;
// Return input context back.
(v, res)
})
.retry(ExponentialBuilder::default())
// Passing context in.
.context(test)
.when(|e| e.to_string() == "retryable")
.await;
println!("fetch succeeded: {}", result.unwrap());
Ok(())
}
```
backon-1.2.0/src/docs/examples/with_self.md 0000644 0000000 0000000 00000001216 10461020230 0016724 0 ustar 0000000 0000000 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 {
Ok(reqwest::get(url).await?.text().await?)
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let test = Test;
let content = (|| async { test.fetch("https://www.rust-lang.org").await })
.retry(ExponentialBuilder::default())
.when(|e| e.to_string() == "retryable")
.await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
backon-1.2.0/src/docs/examples/with_specific_error.md 0000644 0000000 0000000 00000000704 10461020230 0020772 0 ustar 0000000 0000000 Retry with specify retryable error by `when`.
```rust
use anyhow::Result;
use backon::ExponentialBuilder;
use backon::Retryable;
async fn fetch() -> Result {
Ok("Hello, World!".to_string())
}
#[tokio::main]
async fn main() -> Result<()> {
let content = fetch
.retry(ExponentialBuilder::default())
.when(|e| e.to_string() == "retryable")
.await?;
println!("fetch succeeded: {}", content);
Ok(())
}
```
backon-1.2.0/src/docs/mod.rs 0000644 0000000 0000000 00000000105 10461020230 0013721 0 ustar 0000000 0000000 //! Docs for the backon crate, like [`examples`].
pub mod examples;
backon-1.2.0/src/lib.rs 0000644 0000000 0000000 00000012625 10461020230 0012772 0 ustar 0000000 0000000 #![doc(
html_logo_url = "https://raw.githubusercontent.com/Xuanwo/backon/main/.github/assets/logo.jpeg"
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
//! [![Build Status]][actions] [![Latest Version]][crates.io] [](https://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
//!
//!
//!
//! Make **retry** like a built-in feature provided by Rust.
//!
//! - **Simple**: Just like a built-in feature: `your_fn.retry(ExponentialBuilder::default()).await`.
//! - **Flexible**: Supports both blocking and async functions.
//! - **Powerful**: Allows control over retry behavior such as [`when`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.when) and [`notify`](https://docs.rs/backon/latest/backon/struct.Retry.html#method.notify).
//! - **Customizable**: Supports custom retry strategies like [exponential](https://docs.rs/backon/latest/backon/struct.ExponentialBuilder.html), [constant](https://docs.rs/backon/latest/backon/struct.ConstantBuilder.html), etc.
//!
//! # Backoff
//!
//! Retry in BackON requires a backoff strategy. BackON will accept a [`BackoffBuilder`] which will generate a new [`Backoff`] for each retry.
//!
//! BackON provides several backoff implementations with reasonable defaults:
//!
//! - [`ConstantBuilder`]: backoff with a constant delay, limited to a specific number of attempts.
//! - [`ExponentialBuilder`]: backoff with an exponential delay, also supports jitter.
//! - [`FibonacciBuilder`]: backoff with a fibonacci delay, also supports jitter.
//!
//! # Sleep
//!
//! Retry in BackON requires an implementation for sleeping. BackON will accept a [`Sleeper`] to pause for a specified duration.
//!
//! BackON employs the following default sleep implementations:
//!
//! - `tokio-sleep`: Utilizes [`TokioSleeper`] within a Tokio context in non-wasm32 environments.
//! - `gloo-timers-sleep`: Utilizes [`GlooTimersSleep`] to pause in wasm32 environments.
//!
//! Users CAN provide a custom implementation if they prefer not to use the default options.
//!
//! If neither feature is enabled nor a custom implementation is provided, BackON will fallback to an empty sleeper. This will cause a panic in the `debug` profile and do nothing in the `release` profile.
//!
//! # Retry
//!
//! For additional examples, please visit [`docs::examples`].
//!
//! ## Retry an async function
//!
//! ```rust
//! use anyhow::Result;
//! use backon::ExponentialBuilder;
//! use backon::Retryable;
//! use core::time::Duration;
//!
//! async fn fetch() -> Result {
//! Ok("hello, world!".to_string())
//! }
//!
//! #[tokio::main]
//! async fn main() -> Result<()> {
//! let content = fetch
//! // Retry with exponential backoff
//! .retry(ExponentialBuilder::default())
//! // Sleep implementation, default to tokio::time::sleep if `tokio-sleep` has been enabled.
//! .sleep(tokio::time::sleep)
//! // When to retry
//! .when(|e| e.to_string() == "EOF")
//! // Notify when retrying
//! .notify(|err: &anyhow::Error, dur: Duration| {
//! println!("retrying {:?} after {:?}", err, dur);
//! })
//! .await?;
//! println!("fetch succeeded: {}", content);
//!
//! Ok(())
//! }
//! ```
//!
//! ## Retry a blocking function
//!
//! ```rust
//! use anyhow::Result;
//! use backon::BlockingRetryable;
//! use backon::ExponentialBuilder;
//! use core::time::Duration;
//!
//! fn fetch() -> Result {
//! Ok("hello, world!".to_string())
//! }
//!
//! fn main() -> Result<()> {
//! let content = fetch
//! // Retry with exponential backoff
//! .retry(ExponentialBuilder::default())
//! // When to retry
//! .when(|e| e.to_string() == "EOF")
//! // Notify when retrying
//! .notify(|err: &anyhow::Error, dur: Duration| {
//! println!("retrying {:?} after {:?}", err, dur);
//! })
//! .call()?;
//! println!("fetch succeeded: {}", content);
//!
//! Ok(())
//! }
//! ```
#![deny(missing_docs)]
#![deny(unused_qualifications)]
#![no_std]
#[cfg(feature = "std-blocking-sleep")]
extern crate std;
extern crate alloc;
mod backoff;
pub use backoff::*;
mod retry;
pub use retry::Retry;
pub use retry::Retryable;
mod retry_with_context;
pub use retry_with_context::RetryWithContext;
pub use retry_with_context::RetryableWithContext;
mod sleep;
pub use sleep::DefaultSleeper;
#[cfg(all(target_arch = "wasm32", feature = "gloo-timers-sleep"))]
pub use sleep::GlooTimersSleep;
pub use sleep::Sleeper;
#[cfg(all(not(target_arch = "wasm32"), feature = "tokio-sleep"))]
pub use sleep::TokioSleeper;
mod blocking_retry;
pub use blocking_retry::{BlockingRetry, BlockingRetryable};
mod blocking_retry_with_context;
pub use blocking_retry_with_context::{BlockingRetryWithContext, BlockingRetryableWithContext};
mod blocking_sleep;
pub use blocking_sleep::BlockingSleeper;
pub use blocking_sleep::DefaultBlockingSleeper;
#[cfg(feature = "std-blocking-sleep")]
pub use blocking_sleep::StdSleeper;
#[cfg(docsrs)]
pub mod docs;
backon-1.2.0/src/retry.rs 0000644 0000000 0000000 00000033055 10461020230 0013371 0 ustar 0000000 0000000 use core::future::Future;
use core::pin::Pin;
use core::task::ready;
use core::task::Context;
use core::task::Poll;
use core::time::Duration;
use crate::backoff::BackoffBuilder;
use crate::sleep::MaybeSleeper;
use crate::Backoff;
use crate::DefaultSleeper;
use crate::Sleeper;
/// Retryable will add retry support for functions that produce futures with results.
///
/// This means all types that implement `FnMut() -> impl Future