use std::time::Duration;
const FIRST_DELAY: Duration = Duration::from_millis(250);
const MAX_DELAY: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReconnectPolicy {
timeout: Duration,
}
impl Default for ReconnectPolicy {
fn default() -> Self {
Self::from_secs(Self::DEFAULT_TIMEOUT_SECS)
}
}
impl ReconnectPolicy {
pub const DEFAULT_TIMEOUT_SECS: u64 = 0;
pub fn from_secs(secs: u64) -> Self {
Self {
timeout: Duration::from_secs(secs),
}
}
pub fn is_disabled(&self) -> bool {
self.timeout.is_zero()
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn schedule(&self) -> Backoff {
Backoff {
remaining: self.timeout,
next: FIRST_DELAY,
}
}
}
#[derive(Debug)]
pub struct Backoff {
remaining: Duration,
next: Duration,
}
impl Iterator for Backoff {
type Item = Duration;
fn next(&mut self) -> Option<Duration> {
if self.remaining.is_zero() {
return None;
}
let delay = self.next.min(self.remaining);
self.remaining -= delay;
self.next = (self.next * 2).min(MAX_DELAY);
Some(delay)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_disabled() {
assert_eq!(ReconnectPolicy::default().timeout(), Duration::ZERO);
assert!(ReconnectPolicy::default().is_disabled());
}
#[test]
fn zero_disables_reconnect() {
let policy = ReconnectPolicy::from_secs(0);
assert!(policy.is_disabled());
assert_eq!(policy.schedule().count(), 0, "no attempts when disabled");
}
#[test]
fn delays_grow_then_cap() {
let delays: Vec<Duration> = ReconnectPolicy::from_secs(30).schedule().take(6).collect();
assert_eq!(
delays,
vec![
Duration::from_millis(250),
Duration::from_millis(500),
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(2),
Duration::from_secs(2),
]
);
}
#[test]
fn total_wait_never_exceeds_the_window() {
for secs in [1, 3, 30, 120] {
let total: Duration = ReconnectPolicy::from_secs(secs).schedule().sum();
assert_eq!(
total,
Duration::from_secs(secs),
"the schedule should use exactly the window for {secs}s"
);
}
}
#[test]
fn short_window_still_gets_an_attempt() {
let delays: Vec<Duration> = ReconnectPolicy::from_secs(1).schedule().collect();
assert_eq!(delays.first(), Some(&Duration::from_millis(250)));
assert!(delays.len() >= 3, "a 1s window should retry a few times");
}
#[test]
fn last_delay_is_clipped_to_the_window() {
let delays: Vec<Duration> = ReconnectPolicy::from_secs(3).schedule().collect();
assert_eq!(delays.last(), Some(&Duration::from_millis(1250)));
}
}