use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use pin_project_lite::pin_project;
use crate::backoff::{Backoff, ExponentialBackoff};
use crate::clock::{Clock, TokioClock};
use crate::error::RetryError;
use crate::shared::{Decision, give_up, should_retry_after, trace_retry};
#[must_use = "a `Retry` does nothing until you `.await` it"]
pub struct Retry<F, B, C, P, Q> {
op: F,
backoff: B,
clock: C,
when: P,
max_elapsed: Option<Duration>,
attempt_timeout: Option<(Duration, Q)>,
}
#[allow(clippy::type_complexity)]
pub fn retry<F, Fut, T, E>(
op: F,
) -> Retry<F, ExponentialBackoff, TokioClock, fn(&E) -> bool, fn() -> E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
Retry {
op,
backoff: ExponentialBackoff::default(),
clock: TokioClock,
when: (|_| true) as fn(&E) -> bool,
max_elapsed: None,
attempt_timeout: None,
}
}
impl<F, B, C, P, Q> Retry<F, B, C, P, Q> {
pub fn backoff<B2>(self, backoff: B2) -> Retry<F, B2, C, P, Q> {
Retry {
backoff,
op: self.op,
when: self.when,
clock: self.clock,
max_elapsed: self.max_elapsed,
attempt_timeout: self.attempt_timeout,
}
}
pub fn clock<C2>(self, clock: C2) -> Retry<F, B, C2, P, Q> {
Retry {
clock,
op: self.op,
backoff: self.backoff,
when: self.when,
max_elapsed: self.max_elapsed,
attempt_timeout: self.attempt_timeout,
}
}
pub fn max_elapsed(mut self, budget: Duration) -> Self {
self.max_elapsed = Some(budget);
self
}
}
impl<F, Fut, T, E, B, C, P, Q> Retry<F, B, C, P, Q>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
pub fn attempt_timeout<Q2>(self, timeout: Duration, on_timeout: Q2) -> Retry<F, B, C, P, Q2>
where
Q2: Fn() -> E,
{
Retry {
attempt_timeout: Some((timeout, on_timeout)),
op: self.op,
backoff: self.backoff,
clock: self.clock,
when: self.when,
max_elapsed: self.max_elapsed,
}
}
pub fn when<P2>(self, predicate: P2) -> Retry<F, B, C, P2, Q>
where
P2: Fn(&E) -> bool,
{
Retry {
when: predicate,
op: self.op,
backoff: self.backoff,
clock: self.clock,
max_elapsed: self.max_elapsed,
attempt_timeout: self.attempt_timeout,
}
}
}
impl<F, Fut, T, E, B, C, P, Q> IntoFuture for Retry<F, B, C, P, Q>
where
C: Clock,
P: Fn(&E) -> bool,
Q: Fn() -> E,
E: std::fmt::Debug,
F: FnMut() -> Fut,
B: Backoff,
Fut: Future<Output = Result<T, E>>,
{
type Output = Result<T, RetryError<E>>;
type IntoFuture = RetryFuture<F, Fut, B, C, P, C::Sleep, Q>;
fn into_future(self) -> Self::IntoFuture {
RetryFuture {
start: None,
state: RetryState::Idle,
retries: 0,
op: self.op,
when: self.when,
clock: self.clock,
backoff: self.backoff,
max_elapsed: self.max_elapsed,
attempt_timeout: self.attempt_timeout,
}
}
}
pin_project! {
pub struct RetryFuture<F, Fut, B, C, P, S, Q> {
op: F,
when: P,
clock: C,
backoff: B,
start: Option<Instant>,
retries: u32,
#[pin]
state: RetryState<Fut, S>,
max_elapsed: Option<Duration>,
attempt_timeout: Option<(Duration, Q)>,
}
}
pin_project! {
#[project = RetryStateProj]
enum RetryState<Fut, S> {
Idle,
Sleeping { #[pin] delay: S },
Attempting { #[pin] fut: Fut, #[pin] deadline: Option<S> },
}
}
impl<F, Fut, T, E, B, C, P, S, Q> Future for RetryFuture<F, Fut, B, C, P, S, Q>
where
B: Backoff,
P: Fn(&E) -> bool,
Q: Fn() -> E,
E: std::fmt::Debug,
F: FnMut() -> Fut,
C: Clock<Sleep = S>,
S: Future<Output = ()>,
Fut: Future<Output = Result<T, E>>,
{
type Output = Result<T, RetryError<E>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.project();
loop {
let next = match this.state.as_mut().project() {
RetryStateProj::Idle => {
*this.start = Some(this.clock.now());
let deadline = this
.attempt_timeout
.as_ref()
.map(|(d, _)| this.clock.sleep(*d));
RetryState::Attempting {
fut: (this.op)(),
deadline,
}
}
RetryStateProj::Attempting { fut, deadline } => {
let err = match fut.poll(cx) {
Poll::Ready(Ok(value)) => return Poll::Ready(Ok(value)),
Poll::Ready(Err(err)) => err,
Poll::Pending => {
match (deadline.as_pin_mut(), this.attempt_timeout.as_ref()) {
(Some(d), Some((_, on_timeout))) => match d.poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(()) => on_timeout(),
},
_ => return Poll::Pending,
}
}
};
{
let elapsed = || {
this.start.map_or(Duration::ZERO, |s| {
this.clock.now().saturating_duration_since(s)
})
};
let step = should_retry_after(
&err,
&*this.when,
&mut *this.backoff,
*this.max_elapsed,
elapsed,
);
match step {
Decision::Retry(delay) => {
*this.retries += 1;
trace_retry(*this.retries, &err, delay);
RetryState::Sleeping {
delay: this.clock.sleep(delay),
}
}
Decision::Stop { reason, elapsed: m } => {
return Poll::Ready(Err(give_up(
err,
*this.retries,
reason,
m,
elapsed,
)));
}
}
}
}
RetryStateProj::Sleeping { delay } => match delay.poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(()) => {
let deadline = this
.attempt_timeout
.as_ref()
.map(|(d, _)| this.clock.sleep(*d));
RetryState::Attempting {
fut: (this.op)(),
deadline,
}
}
},
};
this.state.set(next);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backoff::{ExponentialBackoff, ExponentialBackoffConfig};
use crate::error::StopReason;
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::sync::{Arc, Mutex};
use std::time::Instant;
#[derive(Clone)]
struct MockClock {
start: Instant,
elapsed: Arc<Mutex<Duration>>,
log: Arc<Mutex<Vec<Duration>>>,
now_calls: Arc<AtomicUsize>,
}
impl MockClock {
fn new() -> Self {
Self {
start: Instant::now(),
elapsed: Arc::new(Mutex::new(Duration::ZERO)),
log: Arc::new(Mutex::new(Vec::new())),
now_calls: Arc::new(AtomicUsize::new(0)),
}
}
fn slept(&self) -> Vec<Duration> {
self.log.lock().unwrap().clone()
}
fn now_calls(&self) -> usize {
self.now_calls.load(SeqCst)
}
}
impl Clock for MockClock {
type Sleep = MockSleep;
fn now(&self) -> Instant {
self.now_calls.fetch_add(1, SeqCst);
self.start + *self.elapsed.lock().unwrap()
}
fn sleep(&self, dur: Duration) -> MockSleep {
MockSleep {
dur,
clock: self.clone(),
fired: false,
}
}
}
struct MockSleep {
dur: Duration,
clock: MockClock,
fired: bool,
}
impl Future for MockSleep {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
if !self.fired {
self.fired = true;
let dur = self.dur;
self.clock.log.lock().unwrap().push(dur);
*self.clock.elapsed.lock().unwrap() += dur;
}
Poll::Ready(())
}
}
fn backoff(max_retries: u32) -> ExponentialBackoff {
ExponentialBackoff::new(ExponentialBackoffConfig {
factor: 2,
base: Duration::from_secs(1),
max_retries,
max_delay: Duration::from_secs(100),
})
.unwrap()
}
fn secs(n: u64) -> Duration {
Duration::from_secs(n)
}
#[tokio::test]
async fn succeeds_first_try() {
let clock = MockClock::new();
let result: Result<i32, RetryError<()>> =
retry(|| async { Ok(42) }).clock(clock.clone()).await;
assert_eq!(result.unwrap(), 42);
assert!(clock.slept().is_empty()); }
#[tokio::test]
async fn retries_then_succeeds() {
let clock = MockClock::new();
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let result: Result<i32, RetryError<&str>> = retry(move || {
let a = a.clone();
async move {
let n = a.fetch_add(1, SeqCst);
if n < 2 { Err("boom") } else { Ok(42) }
}
})
.backoff(backoff(5))
.clock(clock.clone())
.await;
assert_eq!(result.unwrap(), 42);
assert_eq!(attempts.load(SeqCst), 3); assert_eq!(clock.slept(), vec![secs(1), secs(2)]); }
#[tokio::test]
async fn stops_on_non_retryable() {
let clock = MockClock::new();
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let result: Result<i32, RetryError<&str>> = retry(move || {
let a = a.clone();
async move {
a.fetch_add(1, SeqCst);
Err("nope")
}
})
.backoff(backoff(5))
.clock(clock.clone())
.when(|_e| false) .await;
let err = result.unwrap_err();
assert_eq!(*err.error(), "nope");
assert_eq!(err.stop_reason(), StopReason::NotRetryable);
assert_eq!(err.attempts(), 1); assert_eq!(err.elapsed(), Duration::ZERO);
assert_eq!(attempts.load(SeqCst), 1); assert!(clock.slept().is_empty());
}
#[tokio::test]
async fn exhausts_retries() {
let clock = MockClock::new();
let result: Result<i32, RetryError<&str>> = retry(|| async { Err("always") })
.backoff(backoff(3))
.clock(clock.clone())
.await;
let err = result.unwrap_err();
assert_eq!(*err.error(), "always");
assert_eq!(err.stop_reason(), StopReason::RetriesExhausted);
assert_eq!(err.attempts(), 4); assert_eq!(err.elapsed(), secs(7)); assert_eq!(clock.slept().len(), 3); }
#[tokio::test]
async fn stops_on_time_budget() {
let clock = MockClock::new();
let result: Result<i32, RetryError<&str>> = retry(|| async { Err("slow") })
.backoff(backoff(100)) .clock(clock.clone())
.max_elapsed(secs(10))
.await;
let err = result.unwrap_err();
assert_eq!(*err.error(), "slow");
assert_eq!(err.stop_reason(), StopReason::MaxElapsed);
assert!(
err.elapsed() < secs(10),
"elapsed must stay under the budget"
);
assert_eq!(clock.slept(), vec![secs(1), secs(2), secs(4)]);
}
#[tokio::test]
async fn op_may_borrow_non_static_data() {
let clock = MockClock::new();
let greeting = String::from("hi");
let attempts = AtomicUsize::new(0);
let out: Result<String, RetryError<&str>> = retry(|| async {
let n = attempts.fetch_add(1, SeqCst);
if n < 2 {
Err("transient")
} else {
Ok(format!("{greeting}!"))
}
})
.backoff(backoff(5))
.clock(clock.clone())
.await;
assert_eq!(out.unwrap(), "hi!");
assert_eq!(attempts.load(SeqCst), 3);
let _ = greeting; }
#[tokio::test(flavor = "current_thread")]
async fn op_may_be_non_send() {
use std::cell::Cell;
use std::rc::Rc;
let clock = MockClock::new();
let shared = Rc::new(Cell::new(0));
let out: Result<i32, RetryError<&str>> = retry({
let shared = shared.clone();
move || {
let shared = shared.clone();
async move {
shared.set(shared.get() + 1);
if shared.get() < 2 {
Err("transient")
} else {
Ok(shared.get())
}
}
}
})
.backoff(backoff(5))
.clock(clock.clone())
.await;
assert_eq!(out.unwrap(), 2);
}
#[tokio::test]
async fn retries_transient_but_stops_on_fatal() {
#[derive(Debug, PartialEq)]
enum ApiError {
Transient,
Fatal,
}
let clock = MockClock::new();
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let out: Result<i32, RetryError<ApiError>> = retry(move || {
let a = a.clone();
async move {
match a.fetch_add(1, SeqCst) {
0 | 1 => Err(ApiError::Transient),
_ => Err(ApiError::Fatal),
}
}
})
.backoff(backoff(10))
.clock(clock.clone())
.when(|e| matches!(e, ApiError::Transient))
.await;
assert_eq!(*out.unwrap_err().error(), ApiError::Fatal);
assert_eq!(attempts.load(SeqCst), 3); assert_eq!(clock.slept(), vec![secs(1), secs(2)]); }
#[tokio::test]
async fn op_can_be_a_plain_fnmut() {
let clock = MockClock::new();
let mut calls = 0;
let out: Result<i32, RetryError<&str>> = retry(|| {
calls += 1;
let n = calls;
async move { if n < 3 { Err("transient") } else { Ok(n) } }
})
.backoff(backoff(5))
.clock(clock.clone())
.await;
assert_eq!(out.unwrap(), 3);
assert_eq!(calls, 3); }
#[tokio::test]
async fn clock_reads_do_not_scale_with_attempts() {
for retries in [3, 30] {
let clock = MockClock::new();
let _: Result<i32, RetryError<&str>> = retry(|| async { Err("x") })
.backoff(backoff(retries))
.clock(clock.clone())
.await;
assert_eq!(clock.now_calls(), 2, "with {retries} retries and no budget");
}
let clock = MockClock::new();
let _: Result<i32, RetryError<&str>> = retry(|| async { Err("x") })
.backoff(backoff(3))
.clock(clock.clone())
.max_elapsed(secs(1000))
.await;
assert_eq!(clock.now_calls(), 5);
let clock = MockClock::new();
let _: Result<i32, RetryError<&str>> = retry(|| async { Err("x") })
.backoff(backoff(100))
.clock(clock.clone())
.max_elapsed(secs(10))
.await;
assert_eq!(clock.now_calls(), 5); }
#[tokio::test]
async fn elapsed_excludes_time_parked_before_the_first_poll() {
let clock = MockClock::new();
let fut = retry(|| async { Err::<i32, _>("x") })
.backoff(backoff(1))
.clock(clock.clone())
.into_future();
clock.sleep(secs(100)).await;
let err = fut.await.unwrap_err();
assert_eq!(err.elapsed(), secs(1)); }
#[tokio::test]
async fn op_that_suspends_is_resumed() {
let clock = MockClock::new();
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let out: Result<i32, RetryError<&str>> = retry(move || {
let a = a.clone();
async move {
tokio::task::yield_now().await; if a.fetch_add(1, SeqCst) < 1 {
Err("transient")
} else {
Ok(7)
}
}
})
.backoff(backoff(5))
.clock(clock.clone())
.await;
assert_eq!(out.unwrap(), 7);
assert_eq!(attempts.load(SeqCst), 2);
}
#[tokio::test(start_paused = true)]
async fn drives_the_real_tokio_timer() {
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let start = tokio::time::Instant::now();
let out: Result<i32, RetryError<&str>> = retry(move || {
let a = a.clone();
async move {
if a.fetch_add(1, SeqCst) < 2 {
Err("transient")
} else {
Ok(9)
}
}
})
.backoff(backoff(5)) .await;
assert_eq!(out.unwrap(), 9);
assert_eq!(attempts.load(SeqCst), 3);
assert_eq!(start.elapsed(), secs(3)); }
#[tokio::test(start_paused = true)]
async fn enforces_time_budget_with_the_real_clock() {
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let out: Result<i32, RetryError<&str>> = retry(move || {
let a = a.clone();
async move {
a.fetch_add(1, SeqCst);
Err("slow")
}
})
.backoff(backoff(100)) .max_elapsed(secs(10))
.await;
assert_eq!(*out.unwrap_err().error(), "slow");
assert_eq!(attempts.load(SeqCst), 4); }
#[tokio::test(start_paused = true)]
async fn cancels_cleanly_inside_a_timeout() {
use std::sync::atomic::AtomicBool;
struct Guard(Arc<AtomicBool>);
impl Drop for Guard {
fn drop(&mut self) {
self.0.store(true, SeqCst);
}
}
let dropped = Arc::new(AtomicBool::new(false));
let d = dropped.clone();
let retrying = retry(move || {
let g = Guard(d.clone());
async move {
let _g = g;
tokio::time::sleep(secs(60)).await; Ok::<i32, &str>(1)
}
});
let outcome = tokio::time::timeout(secs(1), retrying).await;
assert!(outcome.is_err()); assert!(dropped.load(SeqCst)); }
#[tokio::test]
async fn handles_thousands_of_retries() {
let clock = MockClock::new();
let big = ExponentialBackoff::new(ExponentialBackoffConfig {
factor: 1,
base: Duration::from_nanos(1),
max_retries: 5000,
max_delay: Duration::from_nanos(1),
})
.unwrap();
let out: Result<i32, RetryError<&str>> = retry(|| async { Err("always") })
.backoff(big)
.clock(clock.clone())
.await;
assert_eq!(*out.unwrap_err().error(), "always");
assert_eq!(clock.slept().len(), 5000); }
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn many_concurrent_retries_are_independent() {
use tokio::task::JoinSet;
let mut set = JoinSet::new();
for i in 0..200usize {
set.spawn(async move {
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
retry(move || {
let a = a.clone();
async move {
if a.fetch_add(1, SeqCst) < 2 {
Err("transient")
} else {
Ok(i)
}
}
})
.backoff(backoff(5))
.clock(MockClock::new())
.await
});
}
let mut completed = 0;
while let Some(res) = set.join_next().await {
assert!(res.unwrap().is_ok());
completed += 1;
}
assert_eq!(completed, 200);
}
#[tokio::test]
async fn emits_a_tracing_event_per_retry() {
let events = crate::test_support::count_retry_events();
let clock = MockClock::new();
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let out: Result<i32, RetryError<&str>> = retry(move || {
let a = a.clone();
async move {
if a.fetch_add(1, SeqCst) < 2 {
Err("boom")
} else {
Ok(42)
}
}
})
.backoff(backoff(5))
.clock(clock)
.await;
assert_eq!(out.unwrap(), 42);
assert_eq!(events.get(), 2); }
#[tokio::test]
async fn give_up_event_fires_once_and_only_after_a_retry() {
let events = crate::test_support::count_retry_events();
let clock = MockClock::new();
let _: Result<i32, RetryError<&str>> = retry(|| async { Err("x") })
.backoff(backoff(3))
.clock(clock)
.await;
assert_eq!(events.get(), 4); }
#[tokio::test]
async fn non_retryable_first_error_is_silent() {
let events = crate::test_support::count_retry_events();
let clock = MockClock::new();
let _: Result<i32, RetryError<&str>> = retry(|| async { Err("x") })
.backoff(backoff(3))
.clock(clock)
.when(|_| false)
.await;
assert_eq!(events.get(), 0);
}
#[tokio::test]
async fn drives_a_decorrelated_backoff() {
use crate::backoff::{DecorrelatedBackoff, DecorrelatedBackoffConfig};
let clock = MockClock::new();
let out: Result<i32, RetryError<&str>> = retry(|| async { Err("boom") })
.backoff(
DecorrelatedBackoff::with_seed(
DecorrelatedBackoffConfig {
base: secs(1),
max_retries: 4,
max_delay: secs(20),
},
7,
)
.unwrap(),
)
.clock(clock.clone())
.await;
assert_eq!(*out.unwrap_err().error(), "boom");
let slept = clock.slept();
assert_eq!(slept.len(), 4); assert!(
slept.iter().all(|d| *d >= secs(1) && *d <= secs(20)),
"delays escaped [base, max_delay]: {slept:?}"
);
}
#[tokio::test]
async fn attempt_timeout_bounds_an_operation_that_hangs() {
let clock = MockClock::new();
let out: Result<i32, RetryError<&str>> = retry(std::future::pending::<Result<i32, &str>>)
.backoff(backoff(2))
.attempt_timeout(secs(5), || "timed out")
.clock(clock.clone())
.await;
let err = out.unwrap_err();
assert_eq!(*err.error(), "timed out");
assert_eq!(err.attempts(), 3); assert_eq!(err.stop_reason(), StopReason::RetriesExhausted);
assert_eq!(
clock.slept(),
vec![secs(5), secs(1), secs(5), secs(2), secs(5)]
);
}
#[tokio::test]
async fn a_fast_operation_never_sees_the_timeout() {
let clock = MockClock::new();
let out: Result<i32, RetryError<&str>> = retry(|| async { Ok(7) })
.attempt_timeout(secs(5), || "timed out")
.clock(clock.clone())
.await;
assert_eq!(out.unwrap(), 7);
assert!(clock.slept().is_empty());
}
#[tokio::test]
async fn a_returned_error_wins_over_an_expired_deadline() {
let clock = MockClock::new();
let out: Result<i32, RetryError<&str>> = retry(|| async { Err("real error") })
.backoff(backoff(1))
.attempt_timeout(Duration::ZERO, || "timed out")
.clock(clock.clone())
.await;
assert_eq!(*out.unwrap_err().error(), "real error");
}
#[tokio::test]
async fn attempt_timeout_feeds_the_when_predicate() {
let clock = MockClock::new();
let out: Result<i32, RetryError<&str>> = retry(std::future::pending::<Result<i32, &str>>)
.backoff(backoff(5))
.attempt_timeout(secs(5), || "timed out")
.when(|e: &&str| *e != "timed out")
.clock(clock.clone())
.await;
let err = out.unwrap_err();
assert_eq!(err.stop_reason(), StopReason::NotRetryable);
assert_eq!(err.attempts(), 1);
assert_eq!(clock.slept(), vec![secs(5)]); }
#[tokio::test]
async fn accepts_a_borrowed_or_shared_clock() {
let clock = MockClock::new();
let _: Result<i32, RetryError<&str>> = retry(|| async { Err("x") })
.backoff(backoff(2))
.clock(&clock)
.await;
assert_eq!(clock.slept(), vec![secs(1), secs(2)]);
let shared = Arc::new(MockClock::new());
let _: Result<i32, RetryError<&str>> = retry(|| async { Err("x") })
.backoff(backoff(2))
.clock(Arc::clone(&shared))
.await;
assert_eq!(shared.slept(), vec![secs(1), secs(2)]);
}
#[tokio::test]
async fn drives_a_jittered_backoff() {
let clock = MockClock::new();
let out: Result<i32, RetryError<&str>> = retry(|| async { Err("boom") })
.backoff(crate::backoff::Jittered::with_seed(backoff(3), 42))
.clock(clock.clone())
.await;
assert_eq!(*out.unwrap_err().error(), "boom");
let slept = clock.slept();
assert_eq!(slept.len(), 3);
for (d, cap) in slept.iter().zip([secs(1), secs(2), secs(4)]) {
assert!(*d <= cap, "jittered delay {d:?} exceeded {cap:?}");
}
}
}