#![no_std]
#![warn(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum State {
Closed,
Open,
HalfOpen,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CircuitBreaker {
failure_threshold: u32,
success_threshold: u32,
cooldown: u64,
state: State,
failures: u32,
successes: u32,
opened_at: u64,
}
impl CircuitBreaker {
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,
failures: 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 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.failures = 0,
State::HalfOpen => {
self.successes = self.successes.saturating_add(1);
if self.successes >= self.success_threshold {
self.state = State::Closed;
self.failures = 0;
self.successes = 0;
}
}
State::Open => {}
}
}
pub fn on_failure(&mut self, now: u64) {
match self.state {
State::Closed => {
self.failures = self.failures.saturating_add(1);
if self.failures >= 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.failures = 0;
self.successes = 0;
}
pub fn reset(&mut self) {
self.state = State::Closed;
self.failures = 0;
self.successes = 0;
self.opened_at = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_closed_and_allows() {
let mut cb = CircuitBreaker::new(3, 1000);
assert_eq!(cb.state(), State::Closed);
assert!(cb.allow(0));
}
#[test]
fn failures_below_threshold_stay_closed() {
let mut cb = CircuitBreaker::new(3, 1000);
cb.on_failure(0);
cb.on_failure(0);
assert_eq!(cb.state(), State::Closed);
assert!(cb.allow(0));
}
#[test]
fn reaching_threshold_opens_and_rejects() {
let mut cb = CircuitBreaker::new(3, 1000);
for _ in 0..3 {
cb.on_failure(0);
}
assert_eq!(cb.state(), State::Open);
assert!(!cb.allow(500)); }
#[test]
fn success_resets_failure_run_in_closed() {
let mut cb = CircuitBreaker::new(3, 1000);
cb.on_failure(0);
cb.on_failure(0);
cb.on_success();
cb.on_failure(0);
cb.on_failure(0);
assert_eq!(cb.state(), State::Closed); cb.on_failure(0);
assert_eq!(cb.state(), State::Open);
}
#[test]
fn open_transitions_to_half_open_after_cooldown() {
let mut cb = CircuitBreaker::new(1, 1000);
cb.on_failure(0);
assert_eq!(cb.state(), State::Open);
assert!(!cb.allow(999)); assert_eq!(cb.state(), State::Open);
assert!(cb.allow(1000)); assert_eq!(cb.state(), State::HalfOpen);
}
#[test]
fn half_open_success_closes() {
let mut cb = CircuitBreaker::new(1, 1000);
cb.on_failure(0);
assert!(cb.allow(1000));
assert_eq!(cb.state(), State::HalfOpen);
cb.on_success();
assert_eq!(cb.state(), State::Closed);
}
#[test]
fn half_open_failure_reopens_with_new_cooldown() {
let mut cb = CircuitBreaker::new(1, 1000);
cb.on_failure(0);
assert!(cb.allow(1000));
assert_eq!(cb.state(), State::HalfOpen);
cb.on_failure(1000);
assert_eq!(cb.state(), State::Open);
assert!(!cb.allow(1999)); assert!(cb.allow(2000));
assert_eq!(cb.state(), State::HalfOpen);
}
#[test]
fn success_threshold_requires_multiple_successes() {
let mut cb = CircuitBreaker::new(1, 1000).with_success_threshold(2);
cb.on_failure(0);
assert!(cb.allow(1000));
cb.on_success();
assert_eq!(cb.state(), State::HalfOpen); cb.on_success();
assert_eq!(cb.state(), State::Closed); }
#[test]
fn cooldown_zero_allows_immediately() {
let mut cb = CircuitBreaker::new(1, 0);
cb.on_failure(0);
assert_eq!(cb.state(), State::Open);
assert!(cb.allow(0)); assert_eq!(cb.state(), State::HalfOpen);
}
#[test]
fn zero_failure_threshold_is_treated_as_one() {
let mut cb = CircuitBreaker::new(0, 1000);
assert_eq!(cb.failure_threshold(), 1);
cb.on_failure(0);
assert_eq!(cb.state(), State::Open);
}
#[test]
fn backwards_clock_does_not_panic_or_close_early() {
let mut cb = CircuitBreaker::new(1, 1000);
cb.on_failure(10_000);
assert!(!cb.allow(5_000));
assert_eq!(cb.state(), State::Open);
}
#[test]
fn trip_and_reset_are_explicit() {
let mut cb = CircuitBreaker::new(5, 1000);
cb.trip(0);
assert_eq!(cb.state(), State::Open);
cb.reset();
assert_eq!(cb.state(), State::Closed);
assert!(cb.allow(0));
}
#[test]
fn on_outcome_while_open_is_ignored() {
let mut cb = CircuitBreaker::new(1, 1000);
cb.on_failure(0);
let before = cb;
cb.on_success();
cb.on_failure(0);
assert_eq!(cb, before); }
}