use std::time::Duration;
use crate::shutdown::Watcher;
pub const DEFAULT_INITIAL_DELAY: Duration = Duration::from_secs(5);
pub const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(900);
pub const DEFAULT_SLICE: Duration = Duration::from_secs(60);
pub const DEFAULT_FACTOR: u32 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Backoff {
delay: Duration,
attempt: u32,
initial: Duration,
max: Duration,
slice: Duration,
factor: u32,
}
impl Backoff {
#[must_use]
pub const fn new() -> Self {
Self {
delay: DEFAULT_INITIAL_DELAY,
attempt: 0,
initial: DEFAULT_INITIAL_DELAY,
max: DEFAULT_MAX_DELAY,
slice: DEFAULT_SLICE,
factor: DEFAULT_FACTOR,
}
}
#[must_use]
pub const fn with_initial_delay(mut self, initial: Duration) -> Self {
self.initial = initial;
self.delay = initial;
self.clamp()
}
#[must_use]
pub const fn with_max_delay(mut self, max: Duration) -> Self {
self.max = max;
self.clamp()
}
#[must_use]
pub const fn with_slice(mut self, slice: Duration) -> Self {
self.slice = slice;
self
}
#[must_use]
pub const fn with_factor(mut self, factor: u32) -> Self {
self.factor = if factor == 0 { 1 } else { factor };
self
}
const fn clamp(mut self) -> Self {
if self.initial.as_nanos() > self.max.as_nanos() {
self.initial = self.max;
}
if self.delay.as_nanos() > self.max.as_nanos() {
self.delay = self.max;
}
self
}
#[must_use]
pub const fn delay(&self) -> Duration {
self.delay
}
#[must_use]
pub const fn attempt(&self) -> u32 {
self.attempt
}
#[must_use]
pub const fn is_retrying(&self) -> bool {
self.attempt > 0
}
#[must_use]
pub const fn max_delay(&self) -> Duration {
self.max
}
pub const fn failed(&mut self) -> Duration {
let current = self.delay;
let raised = self.delay.saturating_mul(self.factor);
self.delay = if raised.as_nanos() > self.max.as_nanos() {
self.max
} else {
raised
};
self.attempt = self.attempt.saturating_add(1);
current
}
pub const fn succeeded(&mut self) {
self.delay = self.initial;
self.attempt = 0;
}
#[must_use]
pub const fn sleep_slice(&self, remaining: Duration) -> Duration {
if self.slice.is_zero() || remaining.as_nanos() <= self.slice.as_nanos() {
remaining
} else {
self.slice
}
}
pub async fn wait(&self, watcher: &Watcher) -> bool {
let mut remaining = self.delay;
while !remaining.is_zero() {
let slice = self.sleep_slice(remaining);
if !watcher.sleep(slice).await {
return false;
}
remaining = remaining.saturating_sub(slice);
}
true
}
}
impl Default for Backoff {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use crate::shutdown::Shutdown;
use super::*;
#[test]
fn the_default_schedule_starts_at_five_seconds() {
assert_eq!(Backoff::new().delay(), DEFAULT_INITIAL_DELAY);
assert_eq!(Backoff::new().max_delay(), DEFAULT_MAX_DELAY);
}
#[test]
fn the_delay_multiplies_on_each_failure() {
let mut backoff = Backoff::new();
let seen: Vec<u64> = (0..6).map(|_| backoff.failed().as_secs()).collect();
assert_eq!(seen, [5, 10, 20, 40, 80, 160]);
}
#[test]
fn the_delay_stops_at_the_ceiling() {
let mut backoff = Backoff::new();
for _ in 0..50 {
backoff.failed();
}
assert_eq!(backoff.delay(), DEFAULT_MAX_DELAY);
}
#[test]
fn the_ceiling_is_not_overshot_on_the_way_up() {
let mut backoff = Backoff::new();
for _ in 0..7 {
backoff.failed();
}
assert_eq!(backoff.delay(), Duration::from_secs(640));
backoff.failed();
assert_eq!(backoff.delay(), DEFAULT_MAX_DELAY, "clamped rather than 1280");
}
#[test]
fn any_success_resets_it_completely() {
let mut backoff = Backoff::new();
for _ in 0..10 {
backoff.failed();
}
assert!(backoff.is_retrying());
backoff.succeeded();
assert_eq!(backoff.delay(), DEFAULT_INITIAL_DELAY);
assert_eq!(backoff.attempt(), 0);
assert!(!backoff.is_retrying());
}
#[test]
fn failed_reports_the_delay_that_applied_not_the_next_one() {
let mut backoff = Backoff::new();
assert_eq!(backoff.failed(), Duration::from_secs(5));
assert_eq!(backoff.delay(), Duration::from_secs(10));
}
#[test]
fn the_schedule_is_configurable() {
let mut backoff = Backoff::new()
.with_initial_delay(Duration::from_millis(100))
.with_max_delay(Duration::from_secs(1))
.with_factor(3);
assert_eq!(backoff.delay(), Duration::from_millis(100));
assert_eq!(backoff.failed(), Duration::from_millis(100));
assert_eq!(backoff.delay(), Duration::from_millis(300));
backoff.failed();
assert_eq!(backoff.delay(), Duration::from_millis(900));
backoff.failed();
assert_eq!(backoff.delay(), Duration::from_secs(1), "clamped");
backoff.succeeded();
assert_eq!(backoff.delay(), Duration::from_millis(100));
}
#[test]
fn an_initial_longer_than_the_maximum_is_clamped() {
let backoff = Backoff::new()
.with_max_delay(Duration::from_secs(2))
.with_initial_delay(Duration::from_secs(30));
assert_eq!(backoff.delay(), Duration::from_secs(2));
}
#[test]
fn a_factor_of_one_is_a_constant_delay() {
let mut backoff = Backoff::new().with_factor(1);
for _ in 0..5 {
assert_eq!(backoff.failed(), DEFAULT_INITIAL_DELAY);
}
}
#[test]
fn a_factor_of_zero_is_treated_as_one() {
let mut backoff = Backoff::new().with_factor(0);
backoff.failed();
assert_eq!(backoff.delay(), DEFAULT_INITIAL_DELAY);
}
#[test]
fn a_long_wait_is_slept_in_slices() {
let backoff = Backoff::new();
assert_eq!(backoff.sleep_slice(DEFAULT_MAX_DELAY), DEFAULT_SLICE);
assert_eq!(backoff.sleep_slice(Duration::from_secs(61)), DEFAULT_SLICE);
assert_eq!(backoff.sleep_slice(Duration::from_secs(60)), DEFAULT_SLICE);
assert_eq!(
backoff.sleep_slice(Duration::from_secs(1)),
Duration::from_secs(1)
);
assert_eq!(backoff.sleep_slice(Duration::ZERO), Duration::ZERO);
}
#[test]
fn a_zero_slice_disables_slicing() {
let backoff = Backoff::new().with_slice(Duration::ZERO);
assert_eq!(backoff.sleep_slice(DEFAULT_MAX_DELAY), DEFAULT_MAX_DELAY);
}
#[test]
fn a_slice_is_never_zero_while_time_remains() {
for slice in [Duration::ZERO, Duration::from_secs(1), Duration::from_secs(90)] {
let backoff = Backoff::new().with_slice(slice);
assert!(!backoff.sleep_slice(Duration::from_secs(30)).is_zero());
}
}
#[tokio::test(start_paused = true)]
async fn waiting_serves_the_whole_delay_when_nothing_stops_it() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
let mut backoff = Backoff::new();
for _ in 0..20 {
backoff.failed();
}
assert_eq!(backoff.delay(), DEFAULT_MAX_DELAY);
assert!(backoff.wait(&watcher).await, "fifteen minutes, in slices");
}
#[tokio::test(start_paused = true)]
async fn waiting_is_abandoned_when_a_stop_arrives() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
let mut backoff = Backoff::new();
for _ in 0..20 {
backoff.failed();
}
let waiting = tokio::spawn(async move { backoff.wait(&watcher).await });
tokio::task::yield_now().await;
shutdown.stop();
assert!(!waiting.await.expect("the waiting task"));
}
#[tokio::test(start_paused = true)]
async fn a_stop_is_noticed_within_one_slice() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
let backoff = Backoff::new().with_slice(Duration::from_secs(10));
let mut backoff = backoff;
for _ in 0..20 {
backoff.failed();
}
let started = tokio::time::Instant::now();
let waiting = tokio::spawn(async move { backoff.wait(&watcher).await });
tokio::time::sleep(Duration::from_secs(15)).await;
shutdown.stop();
assert!(!waiting.await.expect("the waiting task"));
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_secs(30),
"a stop must be noticed within a slice, not after the whole \
delay; took {elapsed:?}"
);
}
#[tokio::test(start_paused = true)]
async fn a_zero_delay_waits_for_nothing() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
let backoff = Backoff::new().with_initial_delay(Duration::ZERO);
assert!(backoff.wait(&watcher).await);
}
#[test]
fn a_default_backoff_is_a_new_one() {
assert_eq!(Backoff::default(), Backoff::new());
}
}