use crate::error::Error;
use crate::Result;
use std::time::Duration;
#[derive(Clone, Debug)]
pub struct ExponentialBackoffBuilder {
initial_delay: Duration,
maximum_delay: Duration,
scaling: f64,
}
impl ExponentialBackoffBuilder {
pub fn new() -> Self {
Self {
initial_delay: Duration::from_secs(1),
maximum_delay: Duration::from_secs(60),
scaling: 2.0,
}
}
pub fn with_initial_delay<V: Into<Duration>>(mut self, v: V) -> Self {
self.initial_delay = v.into();
self
}
pub fn with_maximum_delay<V: Into<Duration>>(mut self, v: V) -> Self {
self.maximum_delay = v.into();
self
}
pub fn with_scaling<V: Into<f64>>(mut self, v: V) -> Self {
self.scaling = v.into();
self
}
pub fn build(self) -> Result<ExponentialBackoff> {
if let Some(error) = self.validate() {
return Err(error);
}
Ok(ExponentialBackoff {
maximum_delay: self.maximum_delay,
scaling: self.scaling,
initial_delay: self.initial_delay,
})
}
pub fn clamp(self) -> ExponentialBackoff {
let scaling = self.scaling.clamp(1.0, 32.0);
let maximum_delay = self
.maximum_delay
.clamp(Duration::from_secs(1), Duration::from_secs(24 * 60 * 60));
let current_delay = self
.initial_delay
.clamp(Duration::from_millis(1), maximum_delay);
ExponentialBackoff {
initial_delay: current_delay,
maximum_delay,
scaling,
}
}
fn validate(&self) -> Option<crate::error::Error> {
if self.scaling < 1.0 {
return Some(Error::other(format!(
"scaling ({}) must be >= 1.0",
self.scaling
)));
}
if self.initial_delay.is_zero() {
return Some(Error::other(format!(
"initial delay must be greater than zero, got={:?}",
self.initial_delay
)));
}
if self.maximum_delay < self.initial_delay {
return Some(Error::other(format!(
"maximum delay ({:?} must be greater or equal to the initial delay ({:?})",
self.maximum_delay, self.initial_delay
)));
}
None
}
}
impl std::default::Default for ExponentialBackoffBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ExponentialBackoff {
initial_delay: Duration,
maximum_delay: Duration,
scaling: f64,
}
impl ExponentialBackoff {
fn delay(&self, _loop_start: std::time::Instant, attempt_count: u32) -> Duration {
let exp = std::cmp::min(i32::MAX as u32, attempt_count) as i32;
let exp = exp.saturating_sub(1);
let scaling = self.scaling.powi(exp);
if scaling >= self.maximum_delay.div_duration_f64(self.initial_delay) {
self.maximum_delay
} else {
self.initial_delay.mul_f64(scaling)
}
}
fn delay_with_jitter(
&self,
loop_start: std::time::Instant,
attempt_count: u32,
rng: &mut impl rand::Rng,
) -> std::time::Duration {
let delay = self.delay(loop_start, attempt_count);
rng.gen_range(Duration::ZERO..=delay)
}
}
impl std::default::Default for ExponentialBackoff {
fn default() -> Self {
Self {
initial_delay: Duration::from_secs(1),
maximum_delay: Duration::from_secs(60),
scaling: 2.0,
}
}
}
impl crate::polling_backoff_policy::PollingBackoffPolicy for ExponentialBackoff {
fn wait_period(
&self,
loop_start: std::time::Instant,
attempt_count: u32,
) -> std::time::Duration {
self.delay(loop_start, attempt_count)
}
}
impl crate::backoff_policy::BackoffPolicy for ExponentialBackoff {
fn on_failure(
&self,
loop_start: std::time::Instant,
attempt_count: u32,
) -> std::time::Duration {
self.delay_with_jitter(loop_start, attempt_count, &mut rand::thread_rng())
}
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
#[test]
fn exponential_build_errors() -> TestResult {
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::ZERO)
.with_maximum_delay(Duration::from_secs(5))
.build();
assert!(b.is_err(), "{b:?}");
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(10))
.with_maximum_delay(Duration::from_secs(5))
.build();
assert!(b.is_err(), "{b:?}");
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(1))
.with_maximum_delay(Duration::from_secs(60))
.with_scaling(-1.0)
.build();
assert!(b.is_err(), "{b:?}");
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(1))
.with_maximum_delay(Duration::from_secs(60))
.with_scaling(0.0)
.build();
assert!(b.is_err(), "{b:?}");
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::ZERO)
.build();
assert!(b.is_err(), "{b:?}");
Ok(())
}
#[test]
fn exponential_build_limits() -> TestResult {
let _ = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(1))
.with_maximum_delay(Duration::MAX)
.build()?;
let _ = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_nanos(1))
.with_maximum_delay(Duration::MAX)
.build()?;
let _ = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_nanos(1))
.with_maximum_delay(Duration::MAX)
.with_scaling(1.0)
.build()?;
Ok(())
}
#[test]
fn exponential_builder_defaults() -> TestResult {
let _ = ExponentialBackoffBuilder::new().build()?;
let _ = ExponentialBackoffBuilder::default().build()?;
Ok(())
}
#[test_case::test_case(Duration::from_secs(1), Duration::MAX, 0.5; "scaling below range")]
#[test_case::test_case(Duration::from_secs(1), Duration::MAX, 1_000_000.0; "scaling over range")]
#[test_case::test_case(Duration::from_secs(1), Duration::MAX, 8.0; "max over range")]
#[test_case::test_case(Duration::from_secs(1), Duration::ZERO, 8.0; "max below range")]
#[test_case::test_case(Duration::from_secs(10), Duration::ZERO, 8.0; "init over range")]
#[test_case::test_case(Duration::ZERO, Duration::ZERO, 8.0; "init below range")]
fn exponential_clamp(init: Duration, max: Duration, scaling: f64) -> TestResult {
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(init)
.with_maximum_delay(max)
.with_scaling(scaling)
.clamp();
assert_eq!(b.scaling.clamp(1.0, 32.0), b.scaling);
assert_eq!(
b.initial_delay
.clamp(Duration::from_millis(1), b.maximum_delay),
b.initial_delay
);
assert_eq!(
b.maximum_delay
.clamp(b.initial_delay, Duration::from_secs(24 * 60 * 60)),
b.maximum_delay
);
Ok(())
}
#[test]
fn exponential_full_jitter() -> TestResult {
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(10))
.with_maximum_delay(Duration::from_secs(10))
.build()?;
let now = std::time::Instant::now();
let mut rng = rand::rngs::mock::StepRng::new(0, 0);
assert_eq!(b.delay_with_jitter(now, 1, &mut rng), Duration::ZERO);
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX / 2, 0);
assert_eq!(
b.delay_with_jitter(now, 2, &mut rng),
Duration::from_secs(5)
);
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
assert_eq!(
b.delay_with_jitter(now, 3, &mut rng),
Duration::from_secs(10)
);
Ok(())
}
#[test]
fn exponential_scaling() -> TestResult {
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(1))
.with_maximum_delay(Duration::from_secs(4))
.with_scaling(2.0)
.build()?;
let now = std::time::Instant::now();
assert_eq!(b.delay(now, 1), Duration::from_secs(1));
assert_eq!(b.delay(now, 2), Duration::from_secs(2));
assert_eq!(b.delay(now, 3), Duration::from_secs(4));
assert_eq!(b.delay(now, 4), Duration::from_secs(4));
Ok(())
}
#[test]
fn wait_period() -> TestResult {
use crate::polling_backoff_policy::PollingBackoffPolicy;
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(1))
.with_maximum_delay(Duration::from_secs(4))
.with_scaling(2.0)
.build()?;
let now = std::time::Instant::now();
assert_eq!(b.wait_period(now, 1), Duration::from_secs(1));
assert_eq!(b.wait_period(now, 2), Duration::from_secs(2));
assert_eq!(b.wait_period(now, 3), Duration::from_secs(4));
assert_eq!(b.wait_period(now, 4), Duration::from_secs(4));
Ok(())
}
#[test]
fn exponential_scaling_jitter() -> TestResult {
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(1))
.with_maximum_delay(Duration::from_secs(4))
.with_scaling(2.0)
.build()?;
let now = std::time::Instant::now();
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
assert_eq!(
b.delay_with_jitter(now, 1, &mut rng),
Duration::from_secs(1)
);
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
assert_eq!(
b.delay_with_jitter(now, 2, &mut rng),
Duration::from_secs(2)
);
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
assert_eq!(
b.delay_with_jitter(now, 3, &mut rng),
Duration::from_secs(4)
);
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
assert_eq!(
b.delay_with_jitter(now, 4, &mut rng),
Duration::from_secs(4)
);
Ok(())
}
#[test]
fn on_failure() -> TestResult {
use crate::backoff_policy::BackoffPolicy;
let b = ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_secs(1))
.with_maximum_delay(Duration::from_secs(4))
.with_scaling(2.0)
.build()?;
let now = std::time::Instant::now();
let d = b.on_failure(now, 1);
assert!(Duration::ZERO <= d && d <= Duration::from_secs(1), "{d:?}");
let d = b.on_failure(now, 2);
assert!(Duration::ZERO <= d && d <= Duration::from_secs(2), "{d:?}");
let d = b.on_failure(now, 3);
assert!(Duration::ZERO <= d && d <= Duration::from_secs(4), "{d:?}");
let d = b.on_failure(now, 4);
assert!(Duration::ZERO <= d && d <= Duration::from_secs(4), "{d:?}");
let d = b.on_failure(now, 5);
assert!(Duration::ZERO <= d && d <= Duration::from_secs(4), "{d:?}");
Ok(())
}
#[test]
fn default() -> TestResult {
let b = ExponentialBackoff::default();
let now = std::time::Instant::now();
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
let next = 2 * b.delay_with_jitter(now, 1, &mut rng);
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
assert_eq!(b.delay_with_jitter(now, 2, &mut rng), next);
let next = 2 * next;
let mut rng = rand::rngs::mock::StepRng::new(u64::MAX, 0);
assert_eq!(b.delay_with_jitter(now, 3, &mut rng), next);
Ok(())
}
}