use crate::prelude::*;
use std::pin::Pin;
use std::time::Duration;
impl Default for Backoff {
fn default() -> Self {
Self {
max_attempts: 3,
min: Duration::from_millis(100),
max: Duration::from_secs(4),
factor: 2,
#[cfg(feature = "rand")]
jitter: 0.3,
}
}
}
#[derive(Debug, Clone)]
pub struct Backoff {
max_attempts: u32,
min: Duration,
max: Duration,
#[cfg(feature = "rand")]
jitter: f32,
factor: u32,
}
impl Backoff {
#[inline]
pub fn new(
max_attempts: u32,
min: Duration,
max: impl Into<Option<Duration>>,
) -> Self {
Self {
max_attempts,
min,
max: max.into().unwrap_or(Duration::MAX),
#[cfg(feature = "rand")]
jitter: 0.3,
factor: 2,
}
}
pub fn min(&self) -> &Duration { &self.min }
#[inline]
pub fn set_min(&mut self, min: Duration) { self.min = min; }
pub fn max(&self) -> &Duration { &self.max }
#[inline]
pub fn set_max(&mut self, max: Duration) { self.max = max; }
pub fn max_attempts(&self) -> u32 { self.max_attempts }
pub fn set_max_attempts(&mut self, max_attempts: u32) {
self.max_attempts = max_attempts;
}
#[cfg(feature = "rand")]
pub fn jitter(&self) -> f32 { self.jitter }
#[inline]
#[cfg(feature = "rand")]
pub fn set_jitter(&mut self, jitter: f32) {
assert!(
jitter >= 0f32 && jitter <= 1f32,
"<exponential-backoff>: jitter must be between 0 and 1."
);
self.jitter = jitter;
}
pub fn factor(&self) -> u32 { self.factor }
#[inline]
pub fn set_factor(&mut self, factor: u32) { self.factor = factor; }
#[inline]
pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
self.set_max_attempts(max_attempts);
self
}
#[inline]
pub fn with_min(mut self, min: Duration) -> Self {
self.set_min(min);
self
}
#[inline]
pub fn with_max(mut self, max: Duration) -> Self {
self.set_max(max);
self
}
#[cfg(feature = "rand")]
#[inline]
pub fn with_jitter(mut self, jitter: f32) -> Self {
self.set_jitter(jitter);
self
}
#[inline]
pub fn with_factor(mut self, factor: u32) -> Self {
self.set_factor(factor);
self
}
#[inline]
pub fn iter(&self) -> BackoffIter { BackoffIter::new(self.clone()) }
pub fn stream(&self) -> BackoffStream { BackoffStream::new(self.clone()) }
#[cfg(not(target_arch = "wasm32"))]
pub fn retry<T, E, F>(&self, mut op: F) -> Result<T, E>
where
F: FnMut(BackoffFrame) -> Result<T, E>,
{
for frame in self.iter() {
match op(frame) {
Ok(v) => return Ok(v),
Err(err) => match frame.next_attempt {
Some(d) => std::thread::sleep(d),
None => return Err(err),
},
}
}
unreachable!("Backoff::iter must yield at least one frame")
}
pub async fn retry_async<T, E, Fut, F>(&self, mut op: F) -> Result<T, E>
where
F: FnMut(BackoffFrame) -> Fut,
Fut: core::future::Future<Output = Result<T, E>>,
{
let mut stream = self.stream();
while let Some(frame) = stream.next().await {
match op(frame).await {
Ok(v) => return Ok(v),
Err(err) => {
if frame.is_final() {
return Err(err);
}
}
}
}
unreachable!("Backoff::stream must yield at least one frame")
}
}
impl<'b> IntoIterator for &'b Backoff {
type Item = BackoffFrame;
type IntoIter = BackoffIter;
fn into_iter(self) -> Self::IntoIter { Self::IntoIter::new(self.clone()) }
}
impl IntoIterator for Backoff {
type Item = BackoffFrame;
type IntoIter = BackoffIter;
fn into_iter(self) -> Self::IntoIter { Self::IntoIter::new(self) }
}
use std::iter;
#[derive(Debug, Clone)]
pub struct BackoffIter {
inner: Backoff,
#[cfg(feature = "rand")]
rng: rand::rngs::StdRng,
attempts: u32,
}
impl BackoffIter {
pub(crate) fn new(inner: Backoff) -> Self {
Self {
attempts: 0,
#[cfg(feature = "rand")]
rng: {
use rand::SeedableRng;
rand::rngs::StdRng::from_rng(&mut rand::rng())
},
inner,
}
}
}
impl iter::Iterator for BackoffIter {
type Item = BackoffFrame;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.attempts == self.inner.max_attempts {
return None;
}
let attempt_index = self.attempts;
if attempt_index == self.inner.max_attempts - 1 {
self.attempts = self.attempts.saturating_add(1);
return Some(BackoffFrame {
attempt_index,
next_attempt: None,
});
}
let exponent = self.inner.factor.saturating_pow(attempt_index);
let mut duration = self.inner.min.saturating_mul(exponent);
self.attempts = self.attempts.saturating_add(1);
#[cfg(feature = "rand")]
if self.inner.jitter != 0.0 {
use rand::Rng;
let jitter_factor = (self.inner.jitter * 100f32) as u32;
let random = self.rng.random_range(0..jitter_factor * 2);
let mut duration = duration.saturating_mul(100);
if random < jitter_factor {
let jitter = duration.saturating_mul(random) / 100;
duration = duration.saturating_sub(jitter);
} else {
let jitter = duration.saturating_mul(random / 2) / 100;
duration = duration.saturating_add(jitter);
};
duration /= 100;
}
duration = duration.clamp(self.inner.min, self.inner.max);
Some(BackoffFrame {
attempt_index,
next_attempt: Some(duration),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackoffFrame {
pub attempt_index: u32,
pub next_attempt: Option<Duration>,
}
impl std::fmt::Display for BackoffFrame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.next_attempt {
Some(d) => write!(
f,
"Attempt: {}, Next: {}ms",
self.attempt_index,
d.as_millis()
),
None => write!(f, "Attempt: {}, Next: None", self.attempt_index),
}
}
}
impl BackoffFrame {
pub fn is_final(&self) -> bool { self.next_attempt.is_none() }
}
pub struct BackoffStream {
iter: BackoffIter,
current: Option<BackoffFrame>,
#[cfg(target_arch = "wasm32")]
sleeper: Option<Pin<Box<dyn Future<Output = ()> + 'static>>>,
#[cfg(not(target_arch = "wasm32"))]
sleeper: Option<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
}
impl BackoffStream {
fn new(inner: Backoff) -> Self {
let mut iter = BackoffIter::new(inner);
let current = iter.next();
Self {
iter,
current,
sleeper: None,
}
}
}
impl futures::Stream for BackoffStream {
type Item = BackoffFrame;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
if let Some(fut) = self.sleeper.as_mut() {
match fut.as_mut().poll(cx) {
std::task::Poll::Pending => return std::task::Poll::Pending,
std::task::Poll::Ready(()) => {
self.sleeper = None;
self.current = self.iter.next();
}
}
}
match self.current.take() {
None => std::task::Poll::Ready(None),
Some(frame) => {
if let Some(d) = frame.next_attempt {
self.sleeper = Some(Box::pin(crate::time_ext::sleep(d)));
}
std::task::Poll::Ready(Some(frame))
}
}
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[test]
fn iter_without_rand_deterministic() {
let backoff = Backoff::new(
3,
Duration::from_millis(100),
Duration::from_secs(10),
);
let mut it = backoff.iter();
let f0 = it.next().unwrap();
f0.attempt_index.xpect_eq(0);
f0.next_attempt.xpect_eq(Some(Duration::from_millis(100)));
let f1 = it.next().unwrap();
f1.attempt_index.xpect_eq(1);
f1.next_attempt.xpect_eq(Some(Duration::from_millis(200)));
let f2 = it.next().unwrap();
f2.attempt_index.xpect_eq(2);
f2.next_attempt.xpect_none();
it.next().xpect_none();
}
#[test]
fn clamps_to_max() {
let mut backoff = Backoff::new(
5,
Duration::from_millis(500),
Duration::from_millis(900),
);
backoff.set_factor(3);
let mut it = backoff.iter();
let f0 = it.next().unwrap();
f0.attempt_index.xpect_eq(0);
f0.next_attempt.xpect_eq(Some(Duration::from_millis(500)));
let f1 = it.next().unwrap();
f1.attempt_index.xpect_eq(1);
f1.next_attempt.xpect_eq(Some(Duration::from_millis(900)));
let f2 = it.next().unwrap();
f2.attempt_index.xpect_eq(2);
f2.next_attempt.xpect_eq(Some(Duration::from_millis(900)));
let f3 = it.next().unwrap();
f3.attempt_index.xpect_eq(3);
f3.next_attempt.xpect_eq(Some(Duration::from_millis(900)));
let f4 = it.next().unwrap();
f4.attempt_index.xpect_eq(4);
f4.next_attempt.xpect_eq(None);
it.next().xpect_eq(None);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn retry_succeeds_after_failures() {
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
let counter = AtomicU32::new(0);
#[allow(unused_mut)]
let mut backoff = Backoff::new(
3,
Duration::from_millis(1),
Duration::from_millis(10),
);
#[cfg(feature = "rand")]
{
backoff.set_jitter(0.0);
}
let res: Result<u32, ()> = backoff.retry(|_frame| {
let c = counter.fetch_add(1, Ordering::SeqCst);
if c >= 2 { Ok(c) } else { Err(()) }
});
res.unwrap().xpect_eq(2);
(counter.load(Ordering::SeqCst) >= 3).xpect_true();
}
#[crate::test]
async fn retry_async_succeeds_after_failures() {
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
let counter = std::sync::Arc::new(AtomicU32::new(0));
#[allow(unused_mut)]
let mut backoff = Backoff::new(
3,
Duration::from_millis(5),
Duration::from_millis(100),
);
#[cfg(feature = "rand")]
{
backoff.set_jitter(0.0);
}
let start = web_time::Instant::now();
let result = backoff
.retry_async({
let counter = counter.clone();
move |_frame| {
let c = counter.fetch_add(1, Ordering::SeqCst);
async move {
if c >= 2 {
Ok::<u32, ()>(c)
} else {
Err::<u32, ()>(())
}
}
}
})
.await;
result.unwrap().xpect_eq(2);
let elapsed = start.elapsed();
(elapsed >= Duration::from_millis(15)).xpect_true();
}
#[cfg(feature = "rand")]
#[test]
fn iter_with_rand_in_range() {
let backoff = Backoff::new(
3,
Duration::from_millis(100),
Duration::from_secs(10),
);
let mut it = backoff.iter();
let f0 = it.next().unwrap();
let d1 = f0.next_attempt.unwrap();
(d1 >= Duration::from_millis(70) && d1 <= Duration::from_millis(130))
.xpect_true();
let f1 = it.next().unwrap();
let d2 = f1.next_attempt.unwrap();
(d2 >= Duration::from_millis(140) && d2 <= Duration::from_millis(260))
.xpect_true();
let f2 = it.next().unwrap();
f2.next_attempt.xpect_eq(None);
it.next().xpect_eq(None);
}
#[crate::test]
async fn stream_sleeps_and_yields_attempts() {
#[allow(unused_mut)]
let mut backoff = Backoff::new(
3,
Duration::from_millis(5),
Duration::from_millis(100),
);
#[cfg(feature = "rand")]
{
backoff.set_jitter(0.0);
}
let mut stream = backoff.stream();
let start = web_time::Instant::now();
let mut items = Vec::new();
while let Some(frame) = stream.next().await {
println!("NEXT");
items.push(frame);
}
items.len().xpect_eq(3);
items[0].xpect_eq(BackoffFrame {
attempt_index: 0,
next_attempt: Some(Duration::from_millis(5)),
});
items[1].xpect_eq(BackoffFrame {
attempt_index: 1,
next_attempt: Some(Duration::from_millis(10)),
});
items[2].xpect_eq(BackoffFrame {
attempt_index: 2,
next_attempt: None,
});
let elapsed = start.elapsed();
(elapsed >= Duration::from_millis(15)).xpect_true();
}
}