use rand::prelude::*;
use std::time::Duration;
#[allow(missing_copy_implementations)]
#[derive(Debug, Clone)]
pub struct BackoffConfig {
pub init_backoff: Duration,
pub max_backoff: Duration,
pub base: f64,
}
impl Default for BackoffConfig {
fn default() -> Self {
Self {
init_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(15),
base: 2.,
}
}
}
pub struct Backoff {
init_backoff: f64,
next_backoff_secs: f64,
max_backoff_secs: f64,
base: f64,
rng: Option<Box<dyn Rng + Sync + Send>>,
}
impl std::fmt::Debug for Backoff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Backoff")
.field("init_backoff", &self.init_backoff)
.field("next_backoff_secs", &self.next_backoff_secs)
.field("max_backoff_secs", &self.max_backoff_secs)
.field("base", &self.base)
.finish()
}
}
impl Backoff {
pub fn new(config: &BackoffConfig) -> Self {
Self::new_with_rng(config, None)
}
pub fn new_with_rng(config: &BackoffConfig, rng: Option<Box<dyn Rng + Sync + Send>>) -> Self {
let init_backoff = config.init_backoff.as_secs_f64();
Self {
init_backoff,
next_backoff_secs: init_backoff,
max_backoff_secs: config.max_backoff.as_secs_f64(),
base: config.base,
rng,
}
}
pub fn tick(&mut self) -> Duration {
let range = self.init_backoff..(self.next_backoff_secs * self.base);
let rand_backoff = match self.rng.as_mut() {
Some(rng) => rng.random_range(range),
None => rand::random_range(range),
};
let next_backoff = self.max_backoff_secs.min(rand_backoff);
Duration::from_secs_f64(std::mem::replace(&mut self.next_backoff_secs, next_backoff))
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::SmallRng;
#[test]
fn test_backoff() {
let init_backoff_secs = 1.;
let max_backoff_secs = 500.;
let base = 3.;
let config = BackoffConfig {
init_backoff: Duration::from_secs_f64(init_backoff_secs),
max_backoff: Duration::from_secs_f64(max_backoff_secs),
base,
};
let rng: SmallRng = rand::make_rng();
let mut backoff = Backoff::new_with_rng(&config, Some(Box::new(rng)));
for _ in 0..10 {
let tick = backoff.tick().as_secs_f64();
assert!(tick >= init_backoff_secs, "{tick} > {init_backoff_secs}");
assert!(tick <= max_backoff_secs, "{tick} <= {max_backoff_secs}");
}
}
}