use super::{DefaultSleeper, RetryOperation, RetryPolicy, RetryPredicate, Sleeper};
use rand::distributions::{self, Distribution};
use std::{iter::Take, time::Duration};
use tracing::debug;
#[derive(Debug, Clone)]
pub struct ExponentialBackoff<R, S = DefaultSleeper> {
retry_check: R,
sleeper: S,
config: Config,
random_range: distributions::Uniform<f32>,
}
impl<R> ExponentialBackoff<R> {
pub fn new(retry_check: R, config: Config) -> Self {
Self::with_sleeper(retry_check, config, DefaultSleeper::default())
}
}
impl<R, S> ExponentialBackoff<R, S> {
pub fn with_sleeper(retry_check: R, config: Config, sleeper: S) -> Self {
Self {
random_range: config.random_range(),
retry_check,
config,
sleeper,
}
}
}
impl<T, E, R, S> RetryPolicy<T, E> for ExponentialBackoff<R, S>
where
T: ?Sized,
R: RetryPredicate<E> + Clone,
S: Sleeper + Clone,
E: std::fmt::Debug,
{
type RetryOp = ExponentialRetry<R, S>;
fn new_operation(&mut self) -> Self::RetryOp {
ExponentialRetry {
retry_check: self.retry_check.clone(),
sleeper: self.sleeper.clone(),
intervals: ExponentialIter::with_args(
self.config.initial_interval,
self.config.max_interval,
self.config.multiplier,
self.config.max_retries,
self.random_range,
),
}
}
}
pub struct ExponentialRetry<R, S = DefaultSleeper> {
retry_check: R,
sleeper: S,
intervals: ExponentialIter,
}
impl<T, E, R, S> RetryOperation<T, E> for ExponentialRetry<R, S>
where
T: ?Sized,
R: RetryPredicate<E>,
S: Sleeper,
E: std::fmt::Debug,
{
type Sleep = S::Sleep;
fn check_retry(&mut self, _val: &T, error: &E) -> Option<Self::Sleep> {
if self.retry_check.is_retriable(error) {
if let Some(interval) = self.intervals.next() {
debug!(
message = "retrying after error",
?error,
backoff_ms = %interval.as_millis(),
remaining_attempts = match self.intervals.size_hint() {
(_lower_bound, Some(upper_bound)) => upper_bound,
(lower_bound, None) => lower_bound
},
);
return Some(self.sleeper.sleep(interval));
} else {
debug!(message = "exhausted retry attempts", ?error);
}
}
None
}
}
config_default! {
#[derive(Debug, Clone, Copy, serde::Deserialize)]
pub struct Config {
#[serde(with = "humantime_serde")]
@default(Duration::from_millis(10), "Config::default_initial_interval")
pub initial_interval: Duration,
#[serde(with = "humantime_serde")]
@default(Duration::from_secs(60), "Config::default_max_interval")
pub max_interval: Duration,
@default(2.0, "Config::default_multiplier")
pub multiplier: f32,
@default(Some(16), "Config::default_max_retries")
pub max_retries: Option<usize>,
@default(0.5, "Config::default_randomization_factor")
pub randomization_factor: f32,
}
}
impl Config {
fn random_range(&self) -> distributions::Uniform<f32> {
assert!(
(0.0..=1.0).contains(&self.randomization_factor),
"randomization_factor must be between 0.0 and 1.0: {}",
self.randomization_factor
);
distributions::Uniform::from(
(1.0 - self.randomization_factor)..=(1.0 + self.randomization_factor),
)
}
}
#[derive(Debug, Clone)]
pub struct ExponentialIter {
iter: Take<RandomRange<Exponential>>,
}
impl ExponentialIter {
pub fn new(config: Config) -> Self {
Self::with_args(
config.initial_interval,
config.max_interval,
config.multiplier,
config.max_retries,
config.random_range(),
)
}
fn with_args(
initial_interval: Duration,
max_interval: Duration,
multiplier: f32,
max_retries: Option<usize>,
random_range: distributions::Uniform<f32>,
) -> Self {
Self {
iter: RandomRange {
iter: Exponential {
interval: initial_interval,
multiplier,
max_interval,
},
random_range,
}
.take(max_retries.unwrap_or(usize::MAX)),
}
}
}
impl Iterator for ExponentialIter {
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
}
#[derive(Debug, Clone)]
struct Exponential {
interval: Duration,
multiplier: f32,
max_interval: Duration,
}
impl Iterator for Exponential {
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
let next = Duration::min(self.interval.mul_f32(self.multiplier), self.max_interval);
let current = std::mem::replace(&mut self.interval, next);
Some(current)
}
}
#[derive(Debug, Clone)]
struct RandomRange<I> {
iter: I,
random_range: distributions::Uniform<f32>,
}
impl<I> Iterator for RandomRange<I>
where
I: Iterator<Item = Duration>,
{
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
Some(
self.iter
.next()?
.mul_f32(self.random_range.sample(&mut rand::thread_rng())),
)
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::assert_relative_eq;
#[test]
fn iter_no_random() {
let config = Config {
initial_interval: Duration::from_millis(10),
max_interval: Duration::from_millis(1000),
multiplier: 2.0,
max_retries: Some(10),
randomization_factor: 0.0,
};
let iter = ExponentialIter::new(config);
assert_relative_eq!(
&[0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.0, 1.0, 1.0][..],
&iter.map(|t| Duration::as_secs_f32(&t)).collect::<Vec<_>>()[..]
);
}
#[test]
fn iter_random() {
let config = Config {
initial_interval: Duration::from_millis(10),
max_interval: Duration::from_millis(1000),
multiplier: 2.0,
max_retries: Some(10),
randomization_factor: 0.1,
};
let iter = ExponentialIter::new(config);
for (interval, range) in iter.map(|t| Duration::as_secs_f32(&t)).zip(vec![
(0.009..=0.011),
(0.018..=0.022),
(0.036..=0.044),
(0.072..=0.088),
(0.144..=0.176),
(0.288..=0.352),
(0.576..=0.704),
(0.900..=1.100),
(0.900..=1.100),
(0.900..=1.100),
]) {
assert!(range.contains(&interval));
}
}
#[cfg(feature = "tokio")]
#[tokio::test]
async fn check_retry() {
let config = Config {
initial_interval: Duration::from_millis(10),
max_interval: Duration::from_millis(1000),
multiplier: 2.0,
max_retries: Some(3),
randomization_factor: 0.0,
};
#[derive(Debug)]
enum Retriable {
Yes,
No,
}
let mut retry_policy =
ExponentialBackoff::new(|err: &Retriable| matches!(err, Retriable::Yes), config);
let mut operation =
<ExponentialBackoff<_> as RetryPolicy<(), Retriable>>::new_operation(&mut retry_policy);
tokio::time::pause();
let now = tokio::time::Instant::now();
assert!(operation.check_retry(&(), &Retriable::No).is_none());
assert_relative_eq!(
operation
.check_retry(&(), &Retriable::Yes)
.unwrap()
.deadline()
.duration_since(now)
.as_secs_f32(),
Duration::from_millis(10).as_secs_f32(),
);
assert!(operation.check_retry(&(), &Retriable::No).is_none());
assert_relative_eq!(
operation
.check_retry(&(), &Retriable::Yes)
.unwrap()
.deadline()
.duration_since(now)
.as_secs_f32(),
Duration::from_millis(20).as_secs_f32(),
);
assert_relative_eq!(
operation
.check_retry(&(), &Retriable::Yes)
.unwrap()
.deadline()
.duration_since(now)
.as_secs_f32(),
Duration::from_millis(40).as_secs_f32(),
);
assert!(operation.check_retry(&(), &Retriable::Yes).is_none());
assert!(operation.check_retry(&(), &Retriable::No).is_none());
}
}