use std::num::NonZeroU32;
use std::time::Duration;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SupervisionPolicy {
pub restart_backoff_initial: Duration,
pub restart_backoff_max: Duration,
pub restart_backoff_multiplier: NonZeroU32,
pub restart_window: Duration,
pub max_restarts_per_window: NonZeroU32,
pub stop_grace: Duration,
}
impl SupervisionPolicy {
#[must_use]
pub fn backoff_after(&self, restarts_in_window: u32) -> Duration {
let multiplier = self.restart_backoff_multiplier.get();
let mut delay = self.restart_backoff_initial;
for _ in 0..restarts_in_window {
delay = delay.saturating_mul(multiplier);
if delay >= self.restart_backoff_max {
return self.restart_backoff_max;
}
}
delay.min(self.restart_backoff_max)
}
}
pub const UNCOMMISSIONED_REMEDY: &str = "managed-worker supervision is not configured on this \
server: add a `[worker_supervision]` section naming \
`restart_backoff_initial_ms`, `restart_backoff_max_ms`, \
`restart_backoff_multiplier`, `restart_window_ms`, \
`max_restarts_per_window`, and `stop_grace_ms` (there are no defaults — \
the restart discipline is an operator decision), then restart the server";
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use std::time::Duration;
use super::SupervisionPolicy;
fn policy(initial_ms: u64, max_ms: u64, multiplier: u32) -> Option<SupervisionPolicy> {
Some(SupervisionPolicy {
restart_backoff_initial: Duration::from_millis(initial_ms),
restart_backoff_max: Duration::from_millis(max_ms),
restart_backoff_multiplier: NonZeroU32::new(multiplier)?,
restart_window: Duration::from_secs(60),
max_restarts_per_window: NonZeroU32::new(3)?,
stop_grace: Duration::from_secs(1),
})
}
#[test]
fn first_restart_waits_exactly_the_configured_initial_backoff() -> Result<(), &'static str> {
let policy = policy(100, 10_000, 2).ok_or("policy must build")?;
assert_eq!(policy.backoff_after(0), Duration::from_millis(100));
Ok(())
}
#[test]
fn backoff_grows_geometrically_then_clamps_to_the_ceiling() -> Result<(), &'static str> {
let policy = policy(100, 400, 2).ok_or("policy must build")?;
assert_eq!(policy.backoff_after(1), Duration::from_millis(200));
assert_eq!(policy.backoff_after(2), Duration::from_millis(400));
assert_eq!(policy.backoff_after(3), Duration::from_millis(400));
assert_eq!(policy.backoff_after(64), Duration::from_millis(400));
Ok(())
}
#[test]
fn a_multiplier_of_one_keeps_the_initial_delay_forever() -> Result<(), &'static str> {
let policy = policy(250, 10_000, 1).ok_or("policy must build")?;
assert_eq!(policy.backoff_after(0), Duration::from_millis(250));
assert_eq!(policy.backoff_after(9), Duration::from_millis(250));
Ok(())
}
#[test]
fn extreme_growth_saturates_into_the_ceiling() -> Result<(), &'static str> {
let policy = policy(1_000, 30_000, u32::MAX).ok_or("policy must build")?;
assert_eq!(policy.backoff_after(16), Duration::from_secs(30));
Ok(())
}
}