Skip to content

Commit

Permalink
Merge pull request #48 from ihrwein/refact/transient-errors
Browse files Browse the repository at this point in the history
Refact: transient errors
  • Loading branch information
ihrwein authored Dec 14, 2021
2 parents 0bd4890 + c16a378 commit b03b55d
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 29 deletions.
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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);
Expand All @@ -56,6 +56,14 @@ async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {

## 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
Expand All @@ -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)

Expand All @@ -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

Expand Down
112 changes: 105 additions & 7 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,39 @@ pub enum Error<E> {
/// 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<Duration>),
Transient {
err: E,
retry_after: Option<Duration>,
},
}

impl<E> Error<E> {
// 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<E> fmt::Display for Error<E>
Expand All @@ -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),
}
}
}
Expand All @@ -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()
}
Expand All @@ -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(),
}
}

Expand All @@ -69,6 +109,64 @@ where
/// the question mark operator (?) and the `try!` macro to work.
impl<E> From<E> for Error<E> {
fn from(err: E) -> Error<E> {
Error::Transient(err, None)
Error::Transient {
err,
retry_after: None,
}
}
}

impl<E> PartialEq for Error<E>
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,
}
}
}

#[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),
}
);
}
10 changes: 5 additions & 5 deletions src/future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)),
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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{};
Expand Down Expand Up @@ -92,10 +92,10 @@ impl<B, N, S> Retry<B, N, S> {

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)),
}
}
};
Expand Down
8 changes: 4 additions & 4 deletions tests/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit b03b55d

Please sign in to comment.