Circuit Breaker State Machine

Track failures with closed, open, and half-open states so a failing dependency is rejected fast until a cooldown trial succeeds.

RustResilience

Code

snippet
use std::time::{Duration, Instant};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
    Closed,
    Open,
    HalfOpen,
}

struct CircuitBreaker {
    state: State,
    failures: u32,
    threshold: u32,
    cooldown: Duration,
    opened_at: Instant,
}

enum Decision {
    Allow,
    Reject,
}

impl CircuitBreaker {
    fn new(threshold: u32, cooldown: Duration) -> Self {
        Self {
            state: State::Closed,
            failures: 0,
            threshold,
            cooldown,
            opened_at: Instant::now(),
        }
    }

    fn allow(&mut self) -> Decision {
        match self.state {
            State::Closed => Decision::Allow,
            State::Open if self.opened_at.elapsed() >= self.cooldown => {
                self.state = State::HalfOpen;
                Decision::Allow
            }
            State::Open => Decision::Reject,
            State::HalfOpen => Decision::Reject,
        }
    }

    fn record(&mut self, success: bool) {
        if success {
            self.state = State::Closed;
            self.failures = 0;
            return;
        }
        self.failures += 1;
        if self.failures >= self.threshold || self.state == State::HalfOpen {
            self.state = State::Open;
            self.opened_at = Instant::now();
        }
    }
}

fn main() {
    let mut breaker = CircuitBreaker::new(2, Duration::from_millis(50));
    for attempt in 0..4 {
        let allowed = matches!(breaker.allow(), Decision::Allow);
        println!("attempt {attempt}: allowed={allowed}");
        if allowed {
            breaker.record(false);
        }
    }
    std::thread::sleep(Duration::from_millis(60));
    let trial = matches!(breaker.allow(), Decision::Allow);
    println!("after cooldown allowed={trial}");
    breaker.record(true);
}