use std::time::Duration;
use rand::RngCore;
const RETRY_WINDOWS_SECS: [u64; 10] = [1, 2, 3, 5, 8, 12, 18, 27, 41, 60];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RetryDecision {
pub(crate) window: Duration,
pub(crate) delay: Duration,
pub(crate) display_delay_secs: u64,
}
pub(crate) struct ReconnectPolicy<R> {
retry_count: usize,
rng: R,
}
impl ReconnectPolicy<rand::rngs::ThreadRng> {
pub(crate) fn new() -> Self {
Self::new_with_rng(rand::rng())
}
}
impl<R: RngCore> ReconnectPolicy<R> {
pub(crate) fn new_with_rng(rng: R) -> Self {
Self {
retry_count: 0,
rng,
}
}
pub(crate) fn is_fresh(&self) -> bool {
self.retry_count == 0
}
pub(crate) fn reset(&mut self) {
self.retry_count = 0;
}
pub(crate) fn next_retry(&mut self) -> RetryDecision {
let window = Duration::from_secs(RETRY_WINDOWS_SECS[self.retry_count]);
let delay = jitter_delay(window, &mut self.rng);
self.retry_count = usize::min(self.retry_count + 1, RETRY_WINDOWS_SECS.len() - 1);
RetryDecision {
window,
delay,
display_delay_secs: display_delay_secs(delay),
}
}
}
fn jitter_delay<R: RngCore>(window: Duration, rng: &mut R) -> Duration {
let upper_bound_nanos = u64::try_from(window.as_nanos()).expect("retry windows fit in u64");
if upper_bound_nanos == 0 {
return Duration::ZERO;
}
Duration::from_nanos(rng.next_u64() % upper_bound_nanos)
}
fn display_delay_secs(delay: Duration) -> u64 {
let rounded = delay.as_nanos().div_ceil(1_000_000_000);
u64::try_from(rounded.max(1)).expect("display delay fits in u64")
}
#[cfg(test)]
mod tests {
use rand::RngCore;
use super::{RETRY_WINDOWS_SECS, ReconnectPolicy};
#[derive(Debug)]
struct TestRng {
values: Vec<u64>,
next: usize,
}
impl TestRng {
fn new(values: Vec<u64>) -> Self {
Self { values, next: 0 }
}
}
impl RngCore for TestRng {
fn next_u32(&mut self) -> u32 {
self.next_u64() as u32
}
fn next_u64(&mut self) -> u64 {
let value = self.values[self.next];
self.next += 1;
value
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
let mut remaining = dest;
while !remaining.is_empty() {
let bytes = self.next_u64().to_le_bytes();
let count = remaining.len().min(bytes.len());
remaining[..count].copy_from_slice(&bytes[..count]);
remaining = &mut remaining[count..];
}
}
}
#[test]
fn retry_windows_follow_the_documented_sequence_and_cap() {
let mut policy = ReconnectPolicy::new_with_rng(TestRng::new(vec![0; 12]));
let windows = (0..12)
.map(|_| policy.next_retry().window.as_secs())
.collect::<Vec<_>>();
assert_eq!(
windows,
vec![
RETRY_WINDOWS_SECS[0],
RETRY_WINDOWS_SECS[1],
RETRY_WINDOWS_SECS[2],
RETRY_WINDOWS_SECS[3],
RETRY_WINDOWS_SECS[4],
RETRY_WINDOWS_SECS[5],
RETRY_WINDOWS_SECS[6],
RETRY_WINDOWS_SECS[7],
RETRY_WINDOWS_SECS[8],
RETRY_WINDOWS_SECS[9],
RETRY_WINDOWS_SECS[9],
RETRY_WINDOWS_SECS[9],
]
);
}
#[test]
fn retries_use_full_jitter_and_operator_friendly_display_rounding() {
let mut policy =
ReconnectPolicy::new_with_rng(TestRng::new(vec![400_000_000, 1_500_000_000]));
let first_retry = policy.next_retry();
let second_retry = policy.next_retry();
assert_eq!(first_retry.window.as_secs(), 1);
assert_eq!(first_retry.delay.as_millis(), 400);
assert_eq!(first_retry.display_delay_secs, 1);
assert_eq!(second_retry.window.as_secs(), 2);
assert_eq!(second_retry.delay.as_millis(), 1_500);
assert_eq!(second_retry.display_delay_secs, 2);
}
#[test]
fn display_delay_rounds_up_so_logs_do_not_understate_the_wait() {
let mut policy = ReconnectPolicy::new_with_rng(TestRng::new(vec![0, 1_100_000_000]));
let _ = policy.next_retry();
let retry = policy.next_retry();
assert_eq!(retry.window.as_secs(), 2);
assert_eq!(retry.delay.as_millis(), 1_100);
assert_eq!(retry.display_delay_secs, 2);
}
#[test]
fn reset_starts_a_later_outage_from_the_first_window_again() {
let mut policy = ReconnectPolicy::new_with_rng(TestRng::new(vec![0, 0, 0]));
let _ = policy.next_retry();
let _ = policy.next_retry();
policy.reset();
assert!(policy.is_fresh());
assert_eq!(policy.next_retry().window.as_secs(), 1);
}
}