use std::time::{Duration, Instant};
use crate::runtime::supervisor::spec::RestartLimitSpec;
#[derive(Debug, Clone)]
pub struct RestartTracker {
events: std::collections::VecDeque<Instant>,
limit: RestartLimitSpec,
}
impl RestartTracker {
pub fn new(limit: RestartLimitSpec) -> Self {
Self {
events: std::collections::VecDeque::with_capacity((limit.max_restarts as usize) + 1),
limit,
}
}
pub fn total(&self) -> u32 {
self.events.len() as u32
}
pub fn record_and_check(&mut self, now: Instant) -> bool {
let cutoff = now.checked_sub(self.limit.window);
if let Some(cutoff) = cutoff {
while self.events.front().map(|t| *t < cutoff).unwrap_or(false) {
self.events.pop_front();
}
}
self.events.push_back(now);
self.events.len() as u32 > self.limit.max_restarts
}
}
pub fn backoff_for(attempt: u32) -> Duration {
let base_ms: u64 = 500u64.saturating_mul(1u64 << attempt.min(6));
let capped = base_ms.min(30_000);
let jitter = pseudo_jitter(attempt, capped);
Duration::from_millis(capped.saturating_add(jitter))
}
fn pseudo_jitter(attempt: u32, base: u64) -> u64 {
let mut state: u64 = (attempt as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
state ^= base.rotate_left(13);
state ^= state >> 7;
state ^= state << 17;
state & (base / 2)
}
#[cfg(test)]
mod tests {
use super::*;
fn limit(max: u32, window_ms: u64) -> RestartLimitSpec {
RestartLimitSpec {
max_restarts: max,
window: Duration::from_millis(window_ms),
}
}
#[test]
fn fresh_tracker_under_limit() {
let mut t = RestartTracker::new(limit(5, 60_000));
let now = Instant::now();
for _ in 0..5 {
assert!(!t.record_and_check(now));
}
assert_eq!(t.total(), 5);
assert!(t.record_and_check(now));
}
#[test]
fn old_events_age_out() {
let mut t = RestartTracker::new(limit(2, 1_000));
let t0 = Instant::now();
assert!(!t.record_and_check(t0));
assert!(!t.record_and_check(t0));
let later = t0 + Duration::from_secs(2);
assert!(!t.record_and_check(later));
}
#[test]
fn backoff_caps_at_30s() {
for attempt in 0..20 {
let d = backoff_for(attempt);
assert!(
d.as_millis() <= 30_000 + 30_000 / 2 + 1,
"attempt {attempt} = {d:?}"
);
}
}
#[test]
fn backoff_starts_under_a_second() {
let d = backoff_for(0);
assert!(d.as_millis() >= 500);
assert!(d.as_millis() < 1_000);
}
}