blob: 9f5038f0305954dc82e820b78bffa616a31a3e5d [file] [log] [blame]
use std::thread;
use std::time::Duration;
use crate::backoff::BackoffBuilder;
use crate::Backoff;
/// BlockingRetryable will add retry support for functions.
///
/// For example:
///
/// - Functions without extra args:
///
/// ```ignore
/// fn fetch() -> Result<String> {
/// 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<String> {
/// Ok("hello, world!".to_string())
/// }
///
/// fn main() -> Result<()> {
/// let content = fetch.retry(&ExponentialBuilder::default()).call()?;
/// println!("fetch succeeded: {}", content);
///
/// Ok(())
/// }
/// ```
pub trait BlockingRetryable<B: BackoffBuilder, T, E, F: FnMut() -> Result<T, E>> {
/// Generate a new retry
fn retry(self, builder: &B) -> BlockingRetry<B::Backoff, T, E, F>;
}
impl<B, T, E, F> BlockingRetryable<B, T, E, F> for F
where
B: BackoffBuilder,
F: FnMut() -> Result<T, E>,
{
fn retry(self, builder: &B) -> BlockingRetry<B::Backoff, T, E, F> {
BlockingRetry::new(self, builder.build())
}
}
/// Retry struct generated by [`Retryable`].
pub struct BlockingRetry<B: Backoff, T, E, F: FnMut() -> Result<T, E>> {
backoff: B,
retryable: fn(&E) -> bool,
notify: fn(&E, Duration),
f: F,
}
impl<B, T, E, F> BlockingRetry<B, T, E, F>
where
B: Backoff,
F: FnMut() -> Result<T, E>,
{
/// Create a new retry.
fn new(f: F, backoff: B) -> Self {
BlockingRetry {
backoff,
retryable: |_: &E| true,
notify: |_: &E, _: Duration| {},
f,
}
}
/// Set the conditions for retrying.
///
/// If not specified, we treat all errors as retryable.
///
/// # Examples
///
/// ```no_run
/// use anyhow::Result;
/// use backon::BlockingRetryable;
/// use backon::ExponentialBuilder;
///
/// fn fetch() -> Result<String> {
/// 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(mut self, retryable: fn(&E) -> bool) -> Self {
self.retryable = retryable;
self
}
/// Set to notify for everything retrying.
///
/// If not specified, this is a no-op.
///
/// # Examples
///
/// ```no_run
/// use std::time::Duration;
///
/// use anyhow::Result;
/// use backon::BlockingRetryable;
/// use backon::ExponentialBuilder;
///
/// fn fetch() -> Result<String> {
/// Ok("hello, world!".to_string())
/// }
///
/// #[tokio::main]
/// async 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(mut self, notify: fn(&E, Duration)) -> Self {
self.notify = notify;
self
}
/// Call the retried function.
///
/// TODO: implment [`std::ops::FnOnce`] after it stable.
pub fn call(mut self) -> Result<T, E> {
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);
thread::sleep(dur);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use std::time::Duration;
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().unwrap();
*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().unwrap(), 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().unwrap();
*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().unwrap(), 4);
Ok(())
}
}