diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c62540..a44cb35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,18 @@ jobs: RUST_LOG: DEBUG RUST_BACKTRACE: full + nightly-unit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: rustup toolchain install nightly + - name: Test + run: cargo +nightly test --all-features + env: + RUST_LOG: DEBUG + RUST_BACKTRACE: full + RUSTDOCFLAGS: "--cfg docsrs" + wasm-unit: runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index 7c72235..014c2e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,15 @@ name = "backon" repository = "https://github.com/Xuanwo/backon" version = "0.5.0" +[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] gloo-timers-sleep = ["dep:gloo-timers", "gloo-timers/futures"] tokio-sleep = ["dep:tokio", "tokio/time"] diff --git a/README.md b/README.md index 0ec7b33..7e974f2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ BackON -BackON: Make **retry** like a built-in feature provided by Rust. +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. diff --git a/examples/async.rs b/examples/async.rs deleted file mode 100644 index 1a62f8e..0000000 --- a/examples/async.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Result; -use backon::ExponentialBuilder; -use backon::Retryable; - -// For more examples, please see: https://docs.rs/backon/#examples - -async fn fetch() -> Result { - let response = reqwest::get("https://httpbingo.org/unstable?failure_rate=0.7").await?; - if !response.status().is_success() { - println!("{}", response.status()); - anyhow::bail!("some kind of error"); - } - let text = response.text().await?; - Ok(text) -} - -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<()> { - let _ = fetch.retry(ExponentialBuilder::default()).await?; - println!("fetch succeeded"); - - Ok(()) -} diff --git a/examples/blocking.rs b/examples/blocking.rs deleted file mode 100644 index 887f0c7..0000000 --- a/examples/blocking.rs +++ /dev/null @@ -1,21 +0,0 @@ -use anyhow::Result; - -// For more examples, please see: https://docs.rs/backon/#examples - -fn fetch() -> Result { - Ok("hello, world!".to_string()) -} - -// this example does not run on wasm32-unknown-unknown -#[cfg(not(target_arch = "wasm32"))] -fn main() -> Result<()> { - use backon::BlockingRetryable; - - let content = fetch.retry(backon::ExponentialBuilder::default()).call()?; - println!("fetch succeeded: {}", content); - - Ok(()) -} - -#[cfg(target_arch = "wasm32")] -fn main() {} diff --git a/examples/sqlx.rs b/examples/sqlx.rs deleted file mode 100644 index cafa140..0000000 --- a/examples/sqlx.rs +++ /dev/null @@ -1,24 +0,0 @@ -// For more examples, please see: https://docs.rs/backon/#examples - -// this example does not run on wasm32-unknown-unknown -#[cfg(not(target_arch = "wasm32"))] -#[tokio::main] -async fn main() -> anyhow::Result<()> { - use backon::Retryable; - - 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(backon::ExponentialBuilder::default()) - .await?; - - assert_eq!(row.0, 150); - - Ok(()) -} - -#[cfg(target_arch = "wasm32")] -fn main() {} diff --git a/rustfmt.toml b/rustfmt.toml index ef49173..bb916f5 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,6 @@ edition = "2021" reorder_imports = true + # format_code_in_doc_comments = true # imports_granularity = "Item" # group_imports = "StdExternalCrate" diff --git a/src/docs/examples/basic.md b/src/docs/examples/basic.md new file mode 100644 index 0000000..1833761 --- /dev/null +++ b/src/docs/examples/basic.md @@ -0,0 +1,19 @@ +Retry an async function. + +```rust +use backon::ExponentialBuilder; +use backon::Retryable; +use anyhow::Result; + +async fn fetch() -> Result { + Ok("Hello, Workd!".to_string()) +} + +#[tokio::main] +async fn main() -> Result<()> { + let content = fetch.retry(ExponentialBuilder::default()).await?; + + println!("fetch succeeded: {}", content); + Ok(()) +} +``` diff --git a/examples/closure.rs b/src/docs/examples/closure.md similarity index 51% rename from examples/closure.rs rename to src/docs/examples/closure.md index d865330..afaf2f5 100644 --- a/examples/closure.rs +++ b/src/docs/examples/closure.md @@ -1,10 +1,11 @@ -// For more examples, please see: https://docs.rs/backon/#examples +Retry an closure. -// this example does not run on wasm32-unknown-unknown -#[cfg(not(target_arch = "wasm32"))] -fn main() -> anyhow::Result<()> { - use backon::BlockingRetryable; +```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); @@ -13,6 +14,4 @@ fn main() -> anyhow::Result<()> { Ok(()) } - -#[cfg(target_arch = "wasm32")] -fn main() {} +``` diff --git a/src/docs/examples/mod.rs b/src/docs/examples/mod.rs new file mode 100644 index 0000000..e36f0b8 --- /dev/null +++ b/src/docs/examples/mod.rs @@ -0,0 +1,22 @@ +//! Examples of using backon. + +#[doc = include_str!("basic.md")] +pub mod basic {} + +#[doc = include_str!("closure.md")] +pub mod closure {} + +#[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 {} diff --git a/src/docs/examples/sqlx.md b/src/docs/examples/sqlx.md new file mode 100644 index 0000000..bd6febf --- /dev/null +++ b/src/docs/examples/sqlx.md @@ -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(()) +} +``` diff --git a/src/docs/examples/with_args.md b/src/docs/examples/with_args.md new file mode 100644 index 0000000..82b750a --- /dev/null +++ b/src/docs/examples/with_args.md @@ -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 { + 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(()) + } +``` diff --git a/src/docs/examples/with_mut_self.md b/src/docs/examples/with_mut_self.md new file mode 100644 index 0000000..31b2c47 --- /dev/null +++ b/src/docs/examples/with_mut_self.md @@ -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 { + 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(()) + } +``` diff --git a/src/docs/examples/with_self.md b/src/docs/examples/with_self.md new file mode 100644 index 0000000..9889b5b --- /dev/null +++ b/src/docs/examples/with_self.md @@ -0,0 +1,27 @@ +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(()) + } +``` diff --git a/src/docs/examples/with_specific_error.md b/src/docs/examples/with_specific_error.md new file mode 100644 index 0000000..ef082e9 --- /dev/null +++ b/src/docs/examples/with_specific_error.md @@ -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 { + Ok("Hello, Workd!".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(()) +} +``` diff --git a/src/docs/mod.rs b/src/docs/mod.rs new file mode 100644 index 0000000..02c9324 --- /dev/null +++ b/src/docs/mod.rs @@ -0,0 +1,3 @@ +//! Docs for the backon crate, like examples. + +pub mod examples; diff --git a/src/lib.rs b/src/lib.rs index 1e14d09..941412b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,23 @@ -//! backon intends to provide an opposite backoff implementation of the popular [backoff](https://docs.rs/backoff). +#![doc( + html_logo_url = "https://raw.githubusercontent.com/Xuanwo/backon/main/.github/assets/logo.jpeg" +)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +//! BackON   [![Build Status]][actions] [![Latest Version]][crates.io] [![](https://img.shields.io/discord/1111711408875393035?logo=discord&label=discord)](https://discord.gg/8ARnvtJePD) +//! +//! [Build Status]: https://img.shields.io/github/actions/workflow/status/Xuanwo/backon/ci.yml?branch=main +//! [actions]: https://github.com/Xuanwo/backon/actions?query=branch%3Amain +//! [Latest Version]: https://img.shields.io/crates/v/backon.svg +//! [crates.io]: https://crates.io/crates/backon +//! +//! BackON +//! +//! Make **retry** like a built-in feature provided by Rust. //! -//! - Newer: developed by Rust edition 2021 and latest stable. -//! - Cleaner: Iterator based abstraction, easy to use, customization friendly. -//! - Easier: Trait based implementations, works like a native function provided by closures. +//! - **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 //! @@ -14,143 +29,65 @@ //! - [`ExponentialBackoff`]: backoff with exponential delay, also provides jitter supports. //! - [`FibonacciBackoff`]: backoff with fibonacci delay, also provides jitter supports. //! -//! Internally, `tokio::time::sleep()` will be used to sleep between retries, therefore -//! it will respect [pausing/auto-advancing](https://docs.rs/tokio/latest/tokio/time/fn.pause.html) -//! tokio's Runtime semantics, if enabled. +//! # Retry //! -//! # Examples +//! For more examples, please visit [`docs::examples`]. //! -//! Retry with default settings. +//! ## Retry an async function //! -//! ```no_run +//! ```rust //! use anyhow::Result; //! use backon::ExponentialBuilder; //! use backon::Retryable; +//! use std::time::Duration; //! //! async fn fetch() -> Result { -//! Ok(reqwest::get("https://www.rust-lang.org") -//! .await? -//! .text() -//! .await?) +//! Ok("hello, world!".to_string()) //! } //! -//! #[tokio::main(flavor = "current_thread")] -//! async fn main() -> Result<()> { -//! let content = fetch.retry(ExponentialBuilder::default()).await?; -//! -//! println!("fetch succeeded: {}", content); -//! Ok(()) -//! } -//! ``` -//! -//! Retry with specify retryable error. -//! -//! ```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")] +//! #[tokio::main] //! async fn main() -> Result<()> { //! let content = fetch +//! // Retry with exponential backoff //! .retry(ExponentialBuilder::default()) -//! .when(|e| e.to_string() == "retryable") +//! // 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 functions with args. //! -//! ```no_run -//! 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(()) //! } //! ``` //! -//! Retry functions with receiver `&self`. +//! ## Retry a blocking function //! -//! ```no_run +//! ```rust //! use anyhow::Result; +//! use backon::BlockingRetryable; //! use backon::ExponentialBuilder; -//! use backon::Retryable; -//! -//! struct Test; +//! use std::time::Duration; //! -//! impl Test { -//! async fn fetch(&self, url: &str) -> Result { -//! Ok(reqwest::get(url).await?.text().await?) -//! } +//! fn fetch() -> Result { +//! Ok("hello, world!".to_string()) //! } //! -//! #[tokio::main(flavor = "current_thread")] -//! async fn main() -> Result<()> { -//! let test = Test; -//! let content = (|| async { test.fetch("https://www.rust-lang.org").await }) +//! fn main() -> Result<()> { +//! let content = fetch +//! // Retry with exponential backoff //! .retry(ExponentialBuilder::default()) -//! .when(|e| e.to_string() == "retryable") -//! .await?; -//! +//! // 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 functions with receiver `&mut self`. -//! -//! ```no_run -//! 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(()) //! } //! ``` @@ -190,3 +127,6 @@ mod blocking_retry_with_context; pub use blocking_retry_with_context::BlockingRetryWithContext; #[cfg(not(target_arch = "wasm32"))] pub use blocking_retry_with_context::BlockingRetryableWithContext; + +#[cfg(docsrs)] +pub mod docs; diff --git a/src/retry.rs b/src/retry.rs index 202f878..0d06f03 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -41,7 +41,6 @@ use crate::Sleeper; /// # Examples /// /// For more examples, please see: [https://docs.rs/backon/#examples](https://docs.rs/backon/#examples) -/// pub trait Retryable< B: BackoffBuilder, T, diff --git a/src/sleep.rs b/src/sleep.rs index d694cb2..ebe4091 100644 --- a/src/sleep.rs +++ b/src/sleep.rs @@ -43,6 +43,9 @@ impl Fut, Fut: Future> Sleeper for F { } /// The default implementation of `Sleeper` using `tokio::time::sleep`. +/// +/// it will respect [pausing/auto-advancing](https://docs.rs/tokio/latest/tokio/time/fn.pause.html) +/// tokio's Runtime semantics, if enabled. #[cfg(all(not(target_arch = "wasm32"), feature = "tokio-sleep"))] #[derive(Clone, Copy, Debug, Default)] pub struct TokioSleeper;