use std::collections::VecDeque;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub struct RestartBudgetConfig {
pub window: Duration,
pub max_burst: u32,
pub recovery_rate_per_sec: f64,
}
impl RestartBudgetConfig {
pub fn new(window: Duration, max_burst: u32, recovery_rate_per_sec: f64) -> Self {
Self {
window,
max_burst,
recovery_rate_per_sec,
}
}
pub fn safe_default() -> Self {
Self {
window: Duration::from_secs(60),
max_burst: 10,
recovery_rate_per_sec: 0.5,
}
}
pub fn validate(&self) -> Vec<String> {
let mut warnings = Vec::new();
if self.max_burst > 10_000 {
warnings.push(format!(
"max_burst ({}) exceeds 10_000; memory may reach ~{} bytes",
self.max_burst,
self.max_burst as u64 * 16
));
}
if self.max_burst >= u32::MAX / 2 {
warnings.push(format!(
"max_burst ({}) is dangerously close to u32::MAX; queue would exhaust process memory",
self.max_burst
));
}
if self.recovery_rate_per_sec > 0.0 && self.recovery_rate_per_sec < 0.001 {
warnings.push(format!(
"recovery_rate_per_sec ({}) is below 0.001; budget will effectively never recover",
self.recovery_rate_per_sec
));
}
warnings
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BudgetVerdict {
Granted,
Exhausted {
retry_after_ns: u128,
},
}
#[derive(Debug)]
pub struct RestartBudgetTracker {
config: RestartBudgetConfig,
failures: VecDeque<u128>,
tokens: f64,
last_update_unix_nanos: u128,
}
impl RestartBudgetTracker {
pub fn new(config: RestartBudgetConfig, now_unix_nanos: u128) -> Self {
let max_tokens = config.max_burst as f64;
Self {
config,
failures: VecDeque::new(),
tokens: max_tokens,
last_update_unix_nanos: now_unix_nanos,
}
}
pub fn try_consume(&mut self, now_unix_nanos: u128) -> BudgetVerdict {
self.refill(now_unix_nanos);
self.evict(now_unix_nanos);
if self.tokens >= 1.0 {
self.tokens -= 1.0;
BudgetVerdict::Granted
} else {
let retry_after_ns = self.estimate_retry_ns(now_unix_nanos);
BudgetVerdict::Exhausted { retry_after_ns }
}
}
pub fn current_tokens(&self, _now_unix_nanos: u128) -> f64 {
self.tokens
}
pub fn window_failures(&self, now_unix_nanos: u128) -> u32 {
let window_start = now_unix_nanos.saturating_sub(self.config.window.as_nanos());
self.failures
.iter()
.filter(|&&ts| ts >= window_start)
.count() as u32
}
fn refill(&mut self, now_unix_nanos: u128) {
let elapsed_ns = now_unix_nanos.saturating_sub(self.last_update_unix_nanos);
let elapsed_secs = elapsed_ns as f64 / 1_000_000_000.0;
let recovered = elapsed_secs * self.config.recovery_rate_per_sec;
let max_tokens = self.config.max_burst as f64;
self.tokens = (self.tokens + recovered).min(max_tokens);
self.last_update_unix_nanos = now_unix_nanos;
}
fn evict(&mut self, now_unix_nanos: u128) {
let window_start = now_unix_nanos.saturating_sub(self.config.window.as_nanos());
while let Some(&front) = self.failures.front() {
if front >= window_start {
break;
}
self.failures.pop_front();
}
}
fn estimate_retry_ns(&self, _now_unix_nanos: u128) -> u128 {
if self.config.recovery_rate_per_sec <= 0.0 {
return self.config.window.as_nanos();
}
let deficit = 1.0 - self.tokens;
let secs_needed = deficit / self.config.recovery_rate_per_sec;
(secs_needed * 1_000_000_000.0) as u128
}
}