#![cfg_attr(
feature = "random",
doc = r##"
To apply random jitter to any delay strategy, the [`delay::jitter`] function can be used in
combination with the [`Iterator`] API:
```
# use retry::retry;
# use retry::delay::{Exponential, jitter};
let mut collection = vec![1, 2, 3].into_iter();
let result = retry(Exponential::from_millis(10).map(jitter).take(3), || {
match collection.next() {
Some(n) if n == 3 => Ok("n is 3!"),
Some(_) => Err("n must be 3!"),
None => Err("n was never 3!"),
}
});
assert!(result.is_ok());
```
"##
)]
#![deny(missing_debug_implementations, missing_docs, warnings)]
use std::{
error::Error as StdError,
fmt::{Display, Error as FmtError, Formatter},
thread::sleep,
time::Duration,
};
pub mod delay;
mod opresult;
#[doc(inline)]
pub use opresult::OperationResult;
pub fn retry<I, O, R, E, OR>(iterable: I, mut operation: O) -> Result<R, Error<E>>
where
I: IntoIterator<Item = Duration>,
O: FnMut() -> OR,
OR: Into<OperationResult<R, E>>,
{
retry_with_index(iterable, |_| operation())
}
pub fn retry_with_index<I, O, R, E, OR>(iterable: I, mut operation: O) -> Result<R, Error<E>>
where
I: IntoIterator<Item = Duration>,
O: FnMut(u64) -> OR,
OR: Into<OperationResult<R, E>>,
{
let mut iterator = iterable.into_iter();
let mut current_try = 1;
let mut total_delay = Duration::default();
loop {
match operation(current_try).into() {
OperationResult::Ok(value) => return Ok(value),
OperationResult::Retry(error) => {
if let Some(delay) = iterator.next() {
sleep(delay);
current_try += 1;
total_delay += delay;
} else {
return Err(Error {
error,
total_delay,
tries: current_try,
});
}
}
OperationResult::Err(error) => {
return Err(Error {
error,
total_delay,
tries: current_try,
});
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Error<E> {
pub error: E,
pub total_delay: Duration,
pub tries: u64,
}
impl<E> Display for Error<E>
where
E: Display,
{
fn fmt(&self, formatter: &mut Formatter) -> Result<(), FmtError> {
Display::fmt(&self.error, formatter)
}
}
impl<E> StdError for Error<E>
where
E: StdError,
{
#[allow(deprecated)]
fn description(&self) -> &str {
self.error.description()
}
fn cause(&self) -> Option<&dyn StdError> {
Some(&self.error)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::delay::{Exponential, Fixed, NoDelay};
use super::opresult::OperationResult;
use super::{retry, retry_with_index, Error};
#[test]
fn succeeds_with_infinite_retries() {
let mut collection = vec![1, 2, 3, 4, 5].into_iter();
let value = retry(NoDelay, || match collection.next() {
Some(n) if n == 5 => Ok(n),
Some(_) => Err("not 5"),
None => Err("not 5"),
})
.unwrap();
assert_eq!(value, 5);
}
#[test]
fn succeeds_with_maximum_retries() {
let mut collection = vec![1, 2].into_iter();
let value = retry(NoDelay.take(1), || match collection.next() {
Some(n) if n == 2 => Ok(n),
Some(_) => Err("not 2"),
None => Err("not 2"),
})
.unwrap();
assert_eq!(value, 2);
}
#[test]
fn fails_after_last_try() {
let mut collection = vec![1].into_iter();
let res = retry(NoDelay.take(1), || match collection.next() {
Some(n) if n == 2 => Ok(n),
Some(_) => Err("not 2"),
None => Err("not 2"),
});
assert_eq!(
res,
Err(Error {
error: "not 2",
tries: 2,
total_delay: Duration::from_millis(0)
})
);
}
#[test]
fn fatal_errors() {
let mut collection = vec![1].into_iter();
let res = retry(NoDelay.take(2), || match collection.next() {
Some(n) if n == 2 => OperationResult::Ok(n),
Some(_) => OperationResult::Err("no retry"),
None => OperationResult::Err("not 2"),
});
assert_eq!(
res,
Err(Error {
error: "no retry",
tries: 1,
total_delay: Duration::from_millis(0)
})
);
}
#[test]
fn succeeds_with_fixed_delay() {
let mut collection = vec![1, 2].into_iter();
let value = retry(Fixed::from_millis(1), || match collection.next() {
Some(n) if n == 2 => Ok(n),
Some(_) => Err("not 2"),
None => Err("not 2"),
})
.unwrap();
assert_eq!(value, 2);
}
#[test]
fn fixed_delay_from_duration() {
assert_eq!(
Fixed::from_millis(1_000).next(),
Fixed::from(Duration::from_secs(1)).next(),
);
}
#[test]
fn succeeds_with_exponential_delay() {
let mut collection = vec![1, 2].into_iter();
let value = retry(Exponential::from_millis(1), || match collection.next() {
Some(n) if n == 2 => Ok(n),
Some(_) => Err("not 2"),
None => Err("not 2"),
})
.unwrap();
assert_eq!(value, 2);
}
#[test]
fn succeeds_with_exponential_delay_with_factor() {
let mut collection = vec![1, 2].into_iter();
let value = retry(
Exponential::from_millis_with_factor(1000, 2.0),
|| match collection.next() {
Some(n) if n == 2 => Ok(n),
Some(_) => Err("not 2"),
None => Err("not 2"),
},
)
.unwrap();
assert_eq!(value, 2);
}
#[test]
#[cfg(feature = "random")]
fn succeeds_with_ranged_delay() {
use super::delay::Range;
let mut collection = vec![1, 2].into_iter();
let value = retry(Range::from_millis_exclusive(1, 10), || {
match collection.next() {
Some(n) if n == 2 => Ok(n),
Some(_) => Err("not 2"),
None => Err("not 2"),
}
})
.unwrap();
assert_eq!(value, 2);
}
#[test]
fn succeeds_with_index() {
let mut collection = vec![1, 2, 3].into_iter();
let value = retry_with_index(NoDelay, |current_try| match collection.next() {
Some(n) if n == current_try => Ok(n),
Some(_) => Err("not current_try"),
None => Err("not current_try"),
})
.unwrap();
assert_eq!(value, 1);
}
}