#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum OverloadState {
#[default]
Normal,
Emergency,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct OverloadConfig {
pub enter_drop_rate: f64,
pub recover_drop_rate: f64,
pub recover_windows: u32,
}
impl Default for OverloadConfig {
fn default() -> Self {
Self {
enter_drop_rate: 0.05,
recover_drop_rate: 0.01,
recover_windows: 3,
}
}
}
impl OverloadConfig {
pub fn enter_at(mut self, drop_rate: f64) -> Self {
self.enter_drop_rate = drop_rate;
self
}
pub fn recover_at(mut self, drop_rate: f64, windows: u32) -> Self {
self.recover_drop_rate = drop_rate;
self.recover_windows = windows.max(1);
self
}
}
#[derive(Debug, Clone)]
pub struct OverloadDetector {
config: OverloadConfig,
state: OverloadState,
calm_windows: u32,
}
impl OverloadDetector {
pub fn new(config: OverloadConfig) -> Self {
Self {
config,
state: OverloadState::Normal,
calm_windows: 0,
}
}
pub fn state(&self) -> OverloadState {
self.state
}
pub fn observe(&mut self, drop_rate: f64) -> Option<OverloadState> {
match self.state {
OverloadState::Normal => {
if drop_rate >= self.config.enter_drop_rate {
self.state = OverloadState::Emergency;
self.calm_windows = 0;
return Some(OverloadState::Emergency);
}
None
}
OverloadState::Emergency => {
if drop_rate < self.config.recover_drop_rate {
self.calm_windows += 1;
if self.calm_windows >= self.config.recover_windows {
self.state = OverloadState::Normal;
self.calm_windows = 0;
return Some(OverloadState::Normal);
}
} else {
self.calm_windows = 0;
}
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn det() -> OverloadDetector {
OverloadDetector::new(OverloadConfig::default())
}
#[test]
fn stays_normal_under_budget() {
let mut d = det();
for r in [0.0, 0.01, 0.04, 0.049] {
assert_eq!(d.observe(r), None);
}
assert_eq!(d.state(), OverloadState::Normal);
}
#[test]
fn enters_emergency_on_crossing() {
let mut d = det();
assert_eq!(d.observe(0.06), Some(OverloadState::Emergency));
assert_eq!(d.state(), OverloadState::Emergency);
assert_eq!(d.observe(0.20), None);
}
#[test]
fn requires_sustained_calm_to_recover() {
let mut d = det();
d.observe(0.10); assert_eq!(d.observe(0.005), None);
assert_eq!(d.observe(0.005), None);
assert_eq!(d.observe(0.005), Some(OverloadState::Normal));
}
#[test]
fn a_spike_resets_the_calm_counter_no_flapping() {
let mut d = det();
d.observe(0.10); d.observe(0.005); d.observe(0.005); d.observe(0.08); assert_eq!(d.state(), OverloadState::Emergency);
assert_eq!(d.observe(0.0), None);
assert_eq!(d.observe(0.0), None);
assert_eq!(d.observe(0.0), Some(OverloadState::Normal));
}
#[test]
fn between_recover_and_enter_holds_emergency() {
let mut d = det();
d.observe(0.10); for _ in 0..10 {
assert_eq!(d.observe(0.03), None);
}
assert_eq!(d.state(), OverloadState::Emergency);
}
}