use crate::State;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RollingBreaker<const WINDOW: usize> {
failure_threshold: u32,
success_threshold: u32,
cooldown: u64,
state: State,
outcomes: [bool; WINDOW],
head: usize,
filled: usize,
failures_in_window: u32,
successes: u32,
opened_at: u64,
}
impl<const WINDOW: usize> RollingBreaker<WINDOW> {
pub const fn new(failure_threshold: u32, cooldown: u64) -> Self {
Self {
failure_threshold: if failure_threshold == 0 {
1
} else {
failure_threshold
},
success_threshold: 1,
cooldown,
state: State::Closed,
outcomes: [false; WINDOW],
head: 0,
filled: 0,
failures_in_window: 0,
successes: 0,
opened_at: 0,
}
}
pub const fn with_success_threshold(mut self, success_threshold: u32) -> Self {
self.success_threshold = if success_threshold == 0 {
1
} else {
success_threshold
};
self
}
pub const fn state(&self) -> State {
self.state
}
pub const fn window_size(&self) -> usize {
WINDOW
}
pub const fn failures_in_window(&self) -> u32 {
self.failures_in_window
}
pub const fn failure_threshold(&self) -> u32 {
self.failure_threshold
}
pub const fn success_threshold(&self) -> u32 {
self.success_threshold
}
pub const fn cooldown(&self) -> u64 {
self.cooldown
}
pub fn allow(&mut self, now: u64) -> bool {
if matches!(self.state, State::Open) && now.saturating_sub(self.opened_at) >= self.cooldown
{
self.state = State::HalfOpen;
self.successes = 0;
}
!matches!(self.state, State::Open)
}
pub fn on_success(&mut self) {
match self.state {
State::Closed => self.record(false),
State::HalfOpen => {
self.successes = self.successes.saturating_add(1);
if self.successes >= self.success_threshold {
self.reset();
}
}
State::Open => {}
}
}
pub fn on_failure(&mut self, now: u64) {
match self.state {
State::Closed => {
self.record(true);
if self.failures_in_window >= self.failure_threshold {
self.trip(now);
}
}
State::HalfOpen => self.trip(now),
State::Open => {}
}
}
pub fn trip(&mut self, now: u64) {
self.state = State::Open;
self.opened_at = now;
self.clear_window();
self.successes = 0;
}
pub fn reset(&mut self) {
self.state = State::Closed;
self.clear_window();
self.successes = 0;
self.opened_at = 0;
}
fn record(&mut self, failure: bool) {
if WINDOW == 0 {
return; }
if self.filled == WINDOW {
if self.outcomes[self.head] {
self.failures_in_window = self.failures_in_window.saturating_sub(1);
}
} else {
self.filled += 1;
}
self.outcomes[self.head] = failure;
if failure {
self.failures_in_window = self.failures_in_window.saturating_add(1);
}
self.head = (self.head + 1) % WINDOW;
}
fn clear_window(&mut self) {
self.head = 0;
self.filled = 0;
self.failures_in_window = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trips_on_non_consecutive_failures_in_window() {
let mut b = RollingBreaker::<5>::new(3, 100);
b.on_failure(0);
b.on_success();
b.on_failure(0);
b.on_success();
assert_eq!(b.state(), State::Closed);
assert_eq!(b.failures_in_window(), 2);
b.on_failure(0); assert_eq!(b.state(), State::Open);
}
#[test]
fn old_failures_age_out_of_window() {
let mut b = RollingBreaker::<3>::new(2, 100);
b.on_failure(0); b.on_success(); b.on_success(); b.on_success(); assert_eq!(b.failures_in_window(), 0);
assert_eq!(b.state(), State::Closed);
b.on_failure(0); assert_eq!(b.state(), State::Closed);
b.on_failure(0); assert_eq!(b.state(), State::Open);
}
#[test]
fn half_open_recovers_then_closes() {
let mut b = RollingBreaker::<4>::new(2, 100).with_success_threshold(2);
b.on_failure(0);
b.on_failure(0); assert_eq!(b.state(), State::Open);
assert!(!b.allow(50)); assert!(b.allow(100)); assert_eq!(b.state(), State::HalfOpen);
b.on_success();
assert_eq!(b.state(), State::HalfOpen); b.on_success();
assert_eq!(b.state(), State::Closed);
assert_eq!(b.failures_in_window(), 0); }
#[test]
fn half_open_failure_reopens() {
let mut b = RollingBreaker::<4>::new(2, 100);
b.on_failure(0);
b.on_failure(0);
assert!(b.allow(100)); b.on_failure(200); assert_eq!(b.state(), State::Open);
assert!(!b.allow(250));
}
#[test]
fn backwards_clock_keeps_open_without_panic() {
let mut b = RollingBreaker::<2>::new(1, 100);
b.on_failure(1_000); assert_eq!(b.state(), State::Open);
assert!(!b.allow(0)); assert_eq!(b.state(), State::Open);
}
#[test]
fn zero_window_never_trips_on_rate() {
let mut b = RollingBreaker::<0>::new(1, 100);
for _ in 0..1_000 {
b.on_failure(0);
}
assert_eq!(b.state(), State::Closed);
assert_eq!(b.failures_in_window(), 0);
b.trip(0);
assert_eq!(b.state(), State::Open);
}
#[test]
fn accessors_and_threshold_flooring() {
let b = RollingBreaker::<8>::new(0, 250).with_success_threshold(0);
assert_eq!(b.window_size(), 8);
assert_eq!(b.failure_threshold(), 1); assert_eq!(b.success_threshold(), 1); assert_eq!(b.cooldown(), 250);
assert_eq!(b.state(), State::Closed);
}
#[test]
fn reset_clears_everything() {
let mut b = RollingBreaker::<3>::new(2, 100);
b.on_failure(0);
b.on_failure(0);
assert_eq!(b.state(), State::Open);
b.reset();
assert_eq!(b.state(), State::Closed);
assert_eq!(b.failures_in_window(), 0);
}
}