#![allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Clone, Debug)]
pub struct CircuitBreakerConfig {
pub failure_threshold: u32,
pub success_threshold: u32,
pub reset_timeout_ms: u64,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
success_threshold: 2,
reset_timeout_ms: 5000,
}
}
}
pub struct CircuitBreaker {
pub config: CircuitBreakerConfig,
pub state: CircuitState,
consecutive_failures: u32,
consecutive_successes: u32,
last_opened_at: u64,
}
pub fn new_circuit_breaker(config: CircuitBreakerConfig) -> CircuitBreaker {
CircuitBreaker {
config,
state: CircuitState::Closed,
consecutive_failures: 0,
consecutive_successes: 0,
last_opened_at: 0,
}
}
pub fn is_request_allowed(cb: &mut CircuitBreaker, now_ms: u64) -> bool {
match cb.state {
CircuitState::Closed => true,
CircuitState::HalfOpen => true,
CircuitState::Open => {
if now_ms.saturating_sub(cb.last_opened_at) >= cb.config.reset_timeout_ms {
cb.state = CircuitState::HalfOpen;
cb.consecutive_successes = 0;
true
} else {
false
}
}
}
}
pub fn record_success(cb: &mut CircuitBreaker) {
cb.consecutive_failures = 0;
if cb.state == CircuitState::HalfOpen {
cb.consecutive_successes += 1;
if cb.consecutive_successes >= cb.config.success_threshold {
cb.state = CircuitState::Closed;
}
}
}
pub fn record_failure(cb: &mut CircuitBreaker, now_ms: u64) {
cb.consecutive_successes = 0;
cb.consecutive_failures += 1;
if cb.consecutive_failures >= cb.config.failure_threshold || cb.state == CircuitState::HalfOpen
{
cb.state = CircuitState::Open;
cb.last_opened_at = now_ms;
cb.consecutive_failures = 0;
}
}
pub fn current_state(cb: &CircuitBreaker) -> CircuitState {
cb.state
}
impl CircuitBreaker {
pub fn new(config: CircuitBreakerConfig) -> Self {
new_circuit_breaker(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cb() -> CircuitBreaker {
new_circuit_breaker(CircuitBreakerConfig {
failure_threshold: 3,
success_threshold: 2,
reset_timeout_ms: 1000,
})
}
#[test]
fn test_initial_state_is_closed() {
let cb = make_cb();
assert_eq!(current_state(&cb), CircuitState::Closed);
}
#[test]
fn test_opens_after_threshold_failures() {
let mut cb = make_cb();
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
assert_eq!(current_state(&cb), CircuitState::Open);
}
#[test]
fn test_open_circuit_denies_requests() {
let mut cb = make_cb();
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
assert!(!is_request_allowed(&mut cb, 500));
}
#[test]
fn test_moves_to_half_open_after_timeout() {
let mut cb = make_cb();
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
is_request_allowed(&mut cb, 1500);
assert_eq!(current_state(&cb), CircuitState::HalfOpen);
}
#[test]
fn test_half_open_closes_after_successes() {
let mut cb = make_cb();
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
is_request_allowed(&mut cb, 1500);
record_success(&mut cb);
record_success(&mut cb);
assert_eq!(current_state(&cb), CircuitState::Closed);
}
#[test]
fn test_half_open_failure_reopens() {
let mut cb = make_cb();
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
is_request_allowed(&mut cb, 1500);
record_failure(&mut cb, 1500);
assert_eq!(current_state(&cb), CircuitState::Open);
}
#[test]
fn test_success_in_closed_resets_failure_count() {
let mut cb = make_cb();
record_failure(&mut cb, 0);
record_failure(&mut cb, 0);
record_success(&mut cb);
record_failure(&mut cb, 0);
assert_eq!(current_state(&cb), CircuitState::Closed);
}
#[test]
fn test_closed_allows_requests() {
let mut cb = make_cb();
assert!(is_request_allowed(&mut cb, 0));
}
#[test]
fn test_open_not_expired_denies() {
let mut cb = make_cb();
record_failure(&mut cb, 1000);
record_failure(&mut cb, 1000);
record_failure(&mut cb, 1000);
assert!(!is_request_allowed(&mut cb, 1999));
}
}