From bface84a4afca0cc3817e12b8b9b01aa7b8301aa Mon Sep 17 00:00:00 2001 From: Tibor Date: Thu, 18 Nov 2021 22:02:24 +0000 Subject: [PATCH 1/4] refact: introduce convenience functions to create transient errors --- src/error.rs | 57 +++++++++++++++++++++++++++++++++++++++++++------- src/future.rs | 10 ++++----- src/lib.rs | 5 ++++- src/retry.rs | 8 +++---- tests/retry.rs | 8 +++---- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/error.rs b/src/error.rs index e006639..b4ade01 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,10 +12,39 @@ pub enum Error { /// Permanent means that it's impossible to execute the operation /// successfully. This error is immediately returned from `retry()`. Permanent(E), - /// Transient means that the error is temporary. If the second argument is `None` + + /// Transient means that the error is temporary. If the `retry_after` is `None` /// the operation should be retried according to the backoff policy, else after /// the specified duration. Useful for handling ratelimits like a HTTP 429 response. - Transient(E, Option), + Transient { + err: E, + retry_after: Option, + }, +} + +impl Error { + // Creates an permanent error. + pub fn permanent(err: E) -> Self { + Error::Permanent(err) + } + + // Creates an transient error which is retried according to the backoff + // policy. + pub fn transient(err: E) -> Self { + Error::Transient { + err, + retry_after: None, + } + } + + /// Creates a transient error which is retried after the specified duration. + /// Useful for handling ratelimits like a HTTP 429 response. + pub fn retry_after(err: E, duration: Duration) -> Self { + Error::Transient { + err, + retry_after: Some(duration), + } + } } impl fmt::Display for Error @@ -24,7 +53,11 @@ where { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { - Error::Permanent(ref err) | Error::Transient(ref err, _) => err.fmt(f), + Error::Permanent(ref err) + | Error::Transient { + ref err, + retry_after: _, + } => err.fmt(f), } } } @@ -36,7 +69,10 @@ where fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { let (name, err) = match *self { Error::Permanent(ref err) => ("Permanent", err as &dyn fmt::Debug), - Error::Transient(ref err, _) => ("Transient", err as &dyn fmt::Debug), + Error::Transient { + ref err, + retry_after: _, + } => ("Transient", err as &dyn fmt::Debug), }; f.debug_tuple(name).field(err).finish() } @@ -49,13 +85,17 @@ where fn description(&self) -> &str { match *self { Error::Permanent(_) => "permanent error", - Error::Transient(..) => "transient error", + Error::Transient { .. } => "transient error", } } fn source(&self) -> Option<&(dyn error::Error + 'static)> { match *self { - Error::Permanent(ref err) | Error::Transient(ref err, _) => err.source(), + Error::Permanent(ref err) + | Error::Transient { + ref err, + retry_after: _, + } => err.source(), } } @@ -69,6 +109,9 @@ where /// the question mark operator (?) and the `try!` macro to work. impl From for Error { fn from(err: E) -> Error { - Error::Transient(err, None) + Error::Transient { + err, + retry_after: None, + } } } diff --git a/src/future.rs b/src/future.rs index b7d949a..fe686e1 100644 --- a/src/future.rs +++ b/src/future.rs @@ -75,7 +75,7 @@ where /// /// async fn f() -> Result<(), backoff::Error<&'static str>> { /// // Business logic... -/// Err(backoff::Error::Transient("error", None)) +/// Err(backoff::Error::transient("error")) /// } /// /// # async fn go() { @@ -182,16 +182,16 @@ where match ready!(this.fut.as_mut().poll(cx)) { Ok(v) => return Poll::Ready(Ok(v)), Err(Error::Permanent(e)) => return Poll::Ready(Err(e)), - Err(Error::Transient(e, duration)) => { - match duration.or_else(|| this.backoff.next_backoff()) { + Err(Error::Transient { err, retry_after }) => { + match retry_after.or_else(|| this.backoff.next_backoff()) { Some(duration) => { - this.notify.notify(e, duration); + this.notify.notify(err, duration); this.delay.set(OptionPinned::Some { inner: this.sleeper.sleep(duration), }); this.fut.set((this.operation)()); } - None => return Poll::Ready(Err(e)), + None => return Poll::Ready(Err(err)), } } } diff --git a/src/lib.rs b/src/lib.rs index 21d13a6..0196431 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,9 +104,12 @@ //! //! ## Transient errors //! -//! Transient errors can be constructed by wrapping your error value into `Error::Transient`. +//! Transient errors can be constructed by wrapping your error value into `Error::transient`. //! By using the ? operator or the `try!` macro, you always get transient errors. //! +//! You can also construct transient errors that are retried after a given +//! interval with `Error::retry_after()` - useful for 429 errors. +//! //! `examples/retry.rs`: //! //! ```rust diff --git a/src/retry.rs b/src/retry.rs index 34d09eb..de6de39 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -47,7 +47,7 @@ where /// let notify = |err, dur| { println!("Error happened at {:?}: {}", dur, err); }; /// let f = || -> Result<(), Error<&str>> { /// // Business logic... -/// Err(Error::Transient("error", None)) +/// Err(Error::transient("error")) /// }; /// /// let backoff = Stop{}; @@ -92,10 +92,10 @@ impl Retry { let (err, next) = match err { Error::Permanent(err) => return Err(Error::Permanent(err)), - Error::Transient(err, duration) => { - match duration.or_else(|| self.backoff.next_backoff()) { + Error::Transient { err, retry_after } => { + match retry_after.or_else(|| self.backoff.next_backoff()) { Some(next) => (err, next), - None => return Err(Error::Transient(err, None)), + None => return Err(Error::transient(err)), } } }; diff --git a/tests/retry.rs b/tests/retry.rs index 9978ee3..2a96063 100644 --- a/tests/retry.rs +++ b/tests/retry.rs @@ -17,10 +17,10 @@ fn retry() { return Ok(()); } - Err(Error::Transient( - io::Error::new(io::ErrorKind::Other, "err"), - None, - )) + Err(Error::Transient { + err: io::Error::new(io::ErrorKind::Other, "err"), + retry_after: None, + }) }; let backoff = ExponentialBackoff::default(); From 725b4a09fffa7106466e8545da22d44fe78eced5 Mon Sep 17 00:00:00 2001 From: Tibor Date: Thu, 18 Nov 2021 22:02:41 +0000 Subject: [PATCH 2/4] docs: update README with breaking changes --- README.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3dfd2af..f6c9b6f 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Compile with feature `wasm-bindgen` or `stdweb` for use in WASM environments. `r `backoff` is small crate which allows you to retry operations according to backoff policies. It provides: -* Error type to wrap errors as either transient of permanent, -* different backoff algorithms, including exponential, -* supporting both sync and async code. +- Error type to wrap errors as either transient of permanent, +- different backoff algorithms, including exponential, +- supporting both sync and async code. ## Sync example @@ -29,7 +29,7 @@ Just wrap your fallible operation into a closure, and pass it into `retry`: use backoff::{retry, ExponentialBackoff, Error}; let op = || { - reqwest::blocking::get("http://example.com").map_err(Error::Transient) + reqwest::blocking::get("http://example.com").map_err(Error::transient) }; let _ = retry(&mut ExponentialBackoff::default(), op); @@ -56,6 +56,14 @@ async fn fetch_url(url: &str) -> Result { ## Breaking changes +### 0.3.x -> 0.4.x + +#### Adding new field to Error::Transient + +`Transient` errors got a second field. Useful for handling ratelimits like a HTTP 429 response. + +To fix broken code, just replace calls of `Error::Transient()` with `Error::transient()`. + ### 0.2.x -> 0.3.x #### Removal of Operation trait @@ -76,7 +84,7 @@ The `FutureOperation` trait has been removed. The `retry` and `retry_notify` met #### Changes in feature flags -* `stdweb` flag was removed, as the project is abandoned. +- `stdweb` flag was removed, as the project is abandoned. #### `retry`, `retry_notify` taking ownership of Backoff instances (previously &mut) @@ -85,9 +93,10 @@ The `FutureOperation` trait has been removed. The `retry` and `retry_notify` met ## License Licensed under either of - * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) -at your option. + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + at your option. ### Contribution From 113a0c5fc5abcfc9bcdb221f58ecd9ef6c6728d4 Mon Sep 17 00:00:00 2001 From: Tibor Date: Fri, 19 Nov 2021 09:01:49 +0000 Subject: [PATCH 3/4] feat: impl PartielEq for Error --- src/error.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/error.rs b/src/error.rs index b4ade01..9e0e720 100644 --- a/src/error.rs +++ b/src/error.rs @@ -115,3 +115,27 @@ impl From for Error { } } } + +impl PartialEq for Error +where + E: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Error::Permanent(ref self_err), Error::Permanent(ref other_err)) => { + self_err == other_err + } + ( + Error::Transient { + err: self_err, + retry_after: self_retry_after, + }, + Error::Transient { + err: other_err, + retry_after: other_retry_after, + }, + ) => self_err == other_err && self_retry_after == other_retry_after, + _ => false, + } + } +} From c16a378482b42395c4763946a0a13fe62087fdff Mon Sep 17 00:00:00 2001 From: Tibor Date: Fri, 19 Nov 2021 09:02:18 +0000 Subject: [PATCH 4/4] tests: add tests for creating errors --- src/error.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/error.rs b/src/error.rs index 9e0e720..5c32310 100644 --- a/src/error.rs +++ b/src/error.rs @@ -139,3 +139,34 @@ where } } } + +#[test] +fn create_permanent_error() { + let e = Error::permanent("err"); + assert_eq!(e, Error::Permanent("err")); +} + +#[test] +fn create_transient_error() { + let e = Error::transient("err"); + assert_eq!( + e, + Error::Transient { + err: "err", + retry_after: None + } + ); +} + +#[test] +fn create_transient_error_with_retry_after() { + let retry_after = Duration::from_secs(42); + let e = Error::retry_after("err", retry_after); + assert_eq!( + e, + Error::Transient { + err: "err", + retry_after: Some(retry_after), + } + ); +}