#![doc = "```rust"]
#![doc = include_str!("../examples/blocking.rs")]
#![doc = "```"]
#![cfg_attr(feature = "tokio", doc = "```rust")]
#![cfg_attr(
not(feature = "tokio"),
doc = "```rust,ignore\n\
// Note: example not compiled if `tokio` feature is not enabled.\n"
)]
#![doc = include_str!("../examples/tokio.rs")]
#![doc = "```"]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
use crate::core::EaseOffCore;
use std::cmp;
use std::num::Saturating;
use std::ops::ControlFlow;
use std::time::{Duration, Instant};
#[cfg(feature = "futures")]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-io-2"))))]
pub mod futures;
pub mod core;
mod options;
pub use options::Options;
#[derive(Debug)]
pub struct EaseOff<E> {
core: EaseOffCore,
started_at: Instant,
deadline: Option<Instant>,
num_attempts: Saturating<u32>,
last_error: Option<E>,
next_retry_at: Option<Instant>,
}
impl<E> EaseOff<E> {
#[inline(always)]
pub fn start_unlimited() -> Self {
Options::DEFAULT.start_unlimited()
}
#[inline(always)]
pub fn start_timeout(timeout: Duration) -> Self {
Options::DEFAULT.start_timeout(timeout)
}
#[inline(always)]
pub fn start_timeout_opt(timeout: Option<Duration>) -> Self {
Options::DEFAULT.start_timeout_opt(timeout)
}
#[inline(always)]
pub fn start_deadline(deadline: Instant) -> Self {
Options::DEFAULT.start_deadline(deadline)
}
#[inline(always)]
pub fn start_deadline_opt(deadline: Option<Instant>) -> Self {
Options::DEFAULT.start_deadline_opt(deadline)
}
#[inline(always)]
pub fn started_at(&self) -> Instant {
self.started_at
}
#[inline(always)]
pub fn deadline(&self) -> Option<Instant> {
self.deadline
}
#[inline(always)]
pub fn num_attempts(&self) -> u32 {
self.num_attempts.0
}
fn next_retry_at(&mut self) -> Result<Option<Instant>, Error<E>> {
let now = Instant::now();
let mut rng = rand::thread_rng();
if self.last_error.is_none() {
self.num_attempts = Saturating(0);
return Ok(cmp::max(
self.core
.nth_retry_at(0, now, None, &mut rng)
.expect("passed `None` for deadline, should not be `Err`"),
self.next_retry_at.take(),
));
}
let attempt_num = self.num_attempts.0;
self.num_attempts += 1;
self.core
.nth_retry_at(attempt_num, now, self.deadline, &mut rng)
.map_err(|_e| {
Error::TimedOut(TimeoutError {
last_error: self
.last_error
.take()
.expect("BUG: `last_error` should not be `None` here"),
})
})
.map(|retry_at| cmp::max(retry_at, self.next_retry_at.take()))
}
fn wrap_result<T>(&mut self, result: Result<T, Error<E>>) -> ResultWrapper<'_, T, E> {
ResultWrapper {
result,
ease_off: self,
}
}
}
impl<E> EaseOff<E> {
pub fn try_blocking<T>(
&mut self,
op: impl FnOnce() -> Result<T, E>,
) -> ResultWrapper<'_, T, E> {
match self.next_retry_at() {
Ok(Some(instant)) => {
blocking_sleep_until(instant);
}
Ok(None) => (),
Err(e) => return self.wrap_result(Err(e)),
}
self.wrap_result(op().map_err(Error::MaybeRetryable))
}
}
#[must_use = "`.or_retry()` or `.or_retry_if()` must be called"]
pub struct ResultWrapper<'a, T, E: 'a> {
result: Result<T, Error<E>>,
ease_off: &'a mut EaseOff<E>,
}
impl<'a, T, E: 'a> ResultWrapper<'a, T, E> {
pub fn on_timeout(
self,
on_timeout: impl FnOnce(TimeoutError<E>) -> Error<E>,
) -> ResultWrapper<'a, T, E> {
Self {
result: self.result.map_err(|e| e.on_timeout(on_timeout)),
ease_off: self.ease_off,
}
}
pub fn inspect_err(self, inspect_err: impl FnOnce(&Error<E>)) -> Self {
Self {
result: self.result.inspect_err(inspect_err),
ease_off: self.ease_off,
}
}
pub fn or_retry(self) -> Result<Option<T>, E>
where
E: RetryableError,
{
self.or_retry_if(RetryableError::can_retry)
}
pub fn or_retry_if(self, can_retry: impl FnOnce(&Error<E>) -> bool) -> Result<Option<T>, E> {
self.or_retry_with(|e| {
if can_retry(e) {
ControlFlow::Continue(None)
} else {
ControlFlow::Break(())
}
})
}
pub fn or_retry_with(
self,
should_retry: impl FnOnce(&Error<E>) -> ControlFlow<(), Option<Instant>>,
) -> Result<Option<T>, E> {
match self.result {
Ok(success) => {
self.ease_off.last_error = None;
self.ease_off.next_retry_at = None;
Ok(Some(success))
}
Err(e) => match should_retry(&e) {
ControlFlow::Continue(next_retry_at) => {
self.ease_off.last_error = Some(e.into_inner());
self.ease_off.next_retry_at = next_retry_at;
Ok(None)
}
ControlFlow::Break(()) => Err(e.into_inner()),
},
}
}
}
pub trait RetryableError {
fn can_retry(&self) -> bool;
}
#[derive(Debug)]
pub enum Error<E> {
MaybeRetryable(E),
Fatal(E),
TimedOut(TimeoutError<E>),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct TimeoutError<E> {
pub last_error: E,
}
impl<E: RetryableError> RetryableError for Error<E> {
fn can_retry(&self) -> bool {
match self {
Self::MaybeRetryable(e) => e.can_retry(),
Self::Fatal(_) => false,
Self::TimedOut(_) => false,
}
}
}
impl<E> Error<E> {
pub fn on_timeout(self, on_timeout: impl FnOnce(TimeoutError<E>) -> Self) -> Self {
match self {
Self::TimedOut(e) => on_timeout(e),
other => other,
}
}
pub fn map<E2>(self, map: impl FnOnce(E) -> E2) -> Error<E2> {
match self {
Self::TimedOut(e) => Error::TimedOut(TimeoutError {
last_error: map(e.last_error),
}),
Self::MaybeRetryable(e) => Error::MaybeRetryable(map(e)),
Self::Fatal(e) => Error::Fatal(map(e)),
}
}
pub fn inner(&self) -> &E {
match self {
Self::TimedOut(e) => &e.last_error,
Self::MaybeRetryable(e) => e,
Self::Fatal(e) => e,
}
}
pub fn into_inner(self) -> E {
match self {
Self::TimedOut(e) => e.last_error,
Self::MaybeRetryable(e) => e,
Self::Fatal(e) => e,
}
}
}
fn blocking_sleep_until(instant: Instant) {
let now = Instant::now();
if let Some(sleep_duration) = instant.checked_duration_since(now) {
std::thread::sleep(sleep_duration);
}
}