use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
impl CircuitState {
pub fn as_str(&self) -> &'static str {
match self {
CircuitState::Closed => "closed",
CircuitState::Open => "open",
CircuitState::HalfOpen => "half_open",
}
}
pub fn allows_request(&self) -> bool {
matches!(self, CircuitState::Closed | CircuitState::HalfOpen)
}
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerConfig {
pub failure_threshold: u32,
pub recovery_timeout: Duration,
pub half_open_max_calls: u32,
pub success_threshold: u32,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
recovery_timeout: Duration::from_secs(30),
half_open_max_calls: 3,
success_threshold: 2,
}
}
}
impl CircuitBreakerConfig {
pub fn new(failure_threshold: u32, recovery_timeout: Duration) -> Self {
Self {
failure_threshold,
recovery_timeout,
..Default::default()
}
}
pub fn with_half_open_max_calls(mut self, n: u32) -> Self {
self.half_open_max_calls = n.max(1);
self
}
pub fn with_success_threshold(mut self, n: u32) -> Self {
self.success_threshold = n.max(1);
self
}
}
struct SlaveCircuit {
state: CircuitState,
failure_count: u32,
success_count: u32,
half_open_calls: u32,
opened_at: Option<Instant>,
}
impl SlaveCircuit {
fn new() -> Self {
Self {
state: CircuitState::Closed,
failure_count: 0,
success_count: 0,
half_open_calls: 0,
opened_at: None,
}
}
}
pub struct CircuitBreaker {
config: CircuitBreakerConfig,
circuits: Mutex<HashMap<String, SlaveCircuit>>,
total_requests: AtomicU64,
rejected_requests: AtomicU64,
}
impl CircuitBreaker {
pub fn new(failure_threshold: u32, recovery_timeout: Duration) -> Self {
Self::with_config(CircuitBreakerConfig::new(
failure_threshold,
recovery_timeout,
))
}
pub fn with_config(config: CircuitBreakerConfig) -> Self {
Self {
config,
circuits: Mutex::new(HashMap::new()),
total_requests: AtomicU64::new(0),
rejected_requests: AtomicU64::new(0),
}
}
pub fn with_defaults() -> Self {
Self::with_config(CircuitBreakerConfig::default())
}
pub fn register(&self, slave: &str) {
if let Ok(mut circuits) = self.circuits.lock() {
circuits
.entry(slave.to_string())
.or_insert_with(SlaveCircuit::new);
}
}
pub fn state(&self, slave: &str) -> CircuitState {
match self.circuits.lock() {
Ok(circuits) => {
let c = circuits.get(slave);
match c {
Some(sc) => {
if sc.state == CircuitState::Open {
if let Some(opened_at) = sc.opened_at {
if opened_at.elapsed() >= self.config.recovery_timeout {
return CircuitState::HalfOpen;
}
}
}
sc.state
}
None => CircuitState::Closed,
}
}
Err(_) => CircuitState::Closed,
}
}
pub fn allow_request(&self, slave: &str) -> bool {
self.total_requests.fetch_add(1, Ordering::Relaxed);
let state = self.state(slave);
if !state.allows_request() {
self.rejected_requests.fetch_add(1, Ordering::Relaxed);
return false;
}
if state == CircuitState::HalfOpen {
if let Ok(mut circuits) = self.circuits.lock() {
if let Some(sc) = circuits.get_mut(slave) {
if sc.half_open_calls >= self.config.half_open_max_calls {
self.rejected_requests.fetch_add(1, Ordering::Relaxed);
return false;
}
sc.half_open_calls += 1;
}
}
}
true
}
pub fn record_success(&self, slave: &str) {
if let Ok(mut circuits) = self.circuits.lock() {
let sc = circuits
.entry(slave.to_string())
.or_insert_with(SlaveCircuit::new);
if sc.state == CircuitState::Open {
if let Some(opened_at) = sc.opened_at {
if opened_at.elapsed() >= self.config.recovery_timeout {
sc.state = CircuitState::HalfOpen;
sc.success_count = 0;
sc.half_open_calls = 0;
}
}
}
match sc.state {
CircuitState::Closed => {
sc.failure_count = 0;
}
CircuitState::HalfOpen => {
sc.success_count += 1;
if sc.success_count >= self.config.success_threshold {
sc.state = CircuitState::Closed;
sc.failure_count = 0;
sc.success_count = 0;
sc.half_open_calls = 0;
sc.opened_at = None;
}
}
CircuitState::Open => {}
}
}
}
pub fn record_failure(&self, slave: &str) {
if let Ok(mut circuits) = self.circuits.lock() {
let sc = circuits
.entry(slave.to_string())
.or_insert_with(SlaveCircuit::new);
if sc.state == CircuitState::Open {
if let Some(opened_at) = sc.opened_at {
if opened_at.elapsed() >= self.config.recovery_timeout {
sc.state = CircuitState::HalfOpen;
sc.success_count = 0;
sc.half_open_calls = 0;
}
}
}
match sc.state {
CircuitState::Closed => {
sc.failure_count += 1;
if sc.failure_count >= self.config.failure_threshold {
sc.state = CircuitState::Open;
sc.opened_at = Some(Instant::now());
}
}
CircuitState::HalfOpen => {
sc.state = CircuitState::Open;
sc.opened_at = Some(Instant::now());
sc.success_count = 0;
sc.half_open_calls = 0;
}
CircuitState::Open => {}
}
}
}
pub fn reset(&self, slave: &str) {
if let Ok(mut circuits) = self.circuits.lock() {
if let Some(sc) = circuits.get_mut(slave) {
sc.state = CircuitState::Closed;
sc.failure_count = 0;
sc.success_count = 0;
sc.half_open_calls = 0;
sc.opened_at = None;
}
}
}
pub fn force_open(&self, slave: &str) {
if let Ok(mut circuits) = self.circuits.lock() {
let sc = circuits
.entry(slave.to_string())
.or_insert_with(SlaveCircuit::new);
sc.state = CircuitState::Open;
sc.opened_at = Some(Instant::now());
}
}
pub fn config(&self) -> &CircuitBreakerConfig {
&self.config
}
pub fn total_requests(&self) -> u64 {
self.total_requests.load(Ordering::Relaxed)
}
pub fn rejected_requests(&self) -> u64 {
self.rejected_requests.load(Ordering::Relaxed)
}
pub fn rejection_rate(&self) -> f64 {
let total = self.total_requests();
if total == 0 {
0.0
} else {
self.rejected_requests() as f64 / total as f64
}
}
pub fn list_by_state(&self, state: CircuitState) -> Vec<String> {
match self.circuits.lock() {
Ok(circuits) => circuits
.iter()
.filter(|(_, sc)| {
let effective = if sc.state == CircuitState::Open {
if let Some(opened_at) = sc.opened_at {
if opened_at.elapsed() >= self.config.recovery_timeout {
CircuitState::HalfOpen
} else {
sc.state
}
} else {
sc.state
}
} else {
sc.state
};
effective == state
})
.map(|(k, _)| k.clone())
.collect(),
Err(_) => Vec::new(),
}
}
pub fn open_slaves(&self) -> Vec<String> {
self.list_by_state(CircuitState::Open)
}
pub fn closed_slaves(&self) -> Vec<String> {
self.list_by_state(CircuitState::Closed)
}
pub fn failure_count(&self, slave: &str) -> u32 {
match self.circuits.lock() {
Ok(circuits) => circuits.get(slave).map(|sc| sc.failure_count).unwrap_or(0),
Err(_) => 0,
}
}
pub fn success_count(&self, slave: &str) -> u32 {
match self.circuits.lock() {
Ok(circuits) => circuits.get(slave).map(|sc| sc.success_count).unwrap_or(0),
Err(_) => 0,
}
}
pub fn half_open_slaves(&self) -> Vec<String> {
self.list_by_state(CircuitState::HalfOpen)
}
pub fn registered_slaves(&self) -> Vec<String> {
match self.circuits.lock() {
Ok(circuits) => circuits.keys().cloned().collect(),
Err(_) => Vec::new(),
}
}
pub fn slave_count(&self) -> usize {
match self.circuits.lock() {
Ok(circuits) => circuits.len(),
Err(_) => 0,
}
}
pub fn health_score(&self) -> f64 {
match self.circuits.lock() {
Ok(circuits) => {
if circuits.is_empty() {
return 1.0;
}
let closed = circuits
.values()
.filter(|sc| sc.state == CircuitState::Closed)
.count();
closed as f64 / circuits.len() as f64
}
Err(_) => 0.0,
}
}
pub fn summary(&self) -> String {
let circuits = match self.circuits.lock() {
Ok(c) => c,
Err(_) => return "CircuitBreaker: lock poisoned".to_string(),
};
let mut out = format!(
"CircuitBreaker: {} slave(s), total_req={}, rejected={}, rejection_rate={:.4}\n",
circuits.len(),
self.total_requests(),
self.rejected_requests(),
self.rejection_rate()
);
for (slave, sc) in circuits.iter() {
out.push_str(&format!(
" {} : state={}, failures={}, successes={}\n",
slave,
sc.state.as_str(),
sc.failure_count,
sc.success_count
));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_circuit_state_allows_request() {
assert!(CircuitState::Closed.allows_request());
assert!(!CircuitState::Open.allows_request());
assert!(CircuitState::HalfOpen.allows_request());
}
#[test]
fn test_circuit_state_as_str() {
assert_eq!(CircuitState::Closed.as_str(), "closed");
assert_eq!(CircuitState::Open.as_str(), "open");
assert_eq!(CircuitState::HalfOpen.as_str(), "half_open");
}
#[test]
fn test_new_circuit_breaker_defaults() {
let cb = CircuitBreaker::with_defaults();
assert_eq!(cb.config().failure_threshold, 5);
assert_eq!(cb.config().success_threshold, 2);
assert_eq!(cb.config().half_open_max_calls, 3);
}
#[test]
fn test_unregistered_slave_state_is_closed() {
let cb = CircuitBreaker::with_defaults();
assert_eq!(cb.state("unknown"), CircuitState::Closed);
assert!(cb.allow_request("unknown"));
}
#[test]
fn test_closed_state_allows_requests() {
let cb = CircuitBreaker::new(3, Duration::from_secs(30));
cb.register("s1");
assert_eq!(cb.state("s1"), CircuitState::Closed);
for _ in 0..10 {
assert!(cb.allow_request("s1"));
}
}
#[test]
fn test_failures_below_threshold_stay_closed() {
let cb = CircuitBreaker::new(5, Duration::from_secs(30));
cb.register("s1");
for _ in 0..4 {
cb.record_failure("s1");
}
assert_eq!(cb.state("s1"), CircuitState::Closed);
assert!(cb.allow_request("s1"));
}
#[test]
fn test_failures_at_threshold_opens_circuit() {
let cb = CircuitBreaker::new(3, Duration::from_secs(30));
cb.register("s1");
cb.record_failure("s1");
cb.record_failure("s1");
assert_eq!(cb.state("s1"), CircuitState::Closed);
cb.record_failure("s1");
assert_eq!(cb.state("s1"), CircuitState::Open);
assert!(!cb.allow_request("s1"));
}
#[test]
fn test_open_circuit_rejects_requests() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.record_failure("s1");
assert_eq!(cb.state("s1"), CircuitState::Open);
assert!(!cb.allow_request("s1"));
}
#[test]
fn test_open_transitions_to_half_open_after_timeout() {
let cb = CircuitBreaker::new(1, Duration::from_millis(10));
cb.register("s1");
cb.record_failure("s1");
assert_eq!(cb.state("s1"), CircuitState::Open);
thread::sleep(Duration::from_millis(15));
assert_eq!(cb.state("s1"), CircuitState::HalfOpen);
}
#[test]
fn test_half_open_allows_limited_requests() {
let cb = CircuitBreaker::new(1, Duration::from_millis(10))
.with_config_override(|c| c.half_open_max_calls = 2);
cb.register("s1");
cb.record_failure("s1");
thread::sleep(Duration::from_millis(15));
assert_eq!(cb.state("s1"), CircuitState::HalfOpen);
assert!(cb.allow_request("s1"));
assert!(cb.allow_request("s1"));
assert!(!cb.allow_request("s1"), "third request should be rejected");
}
#[test]
fn test_half_open_success_closes_circuit() {
let cb = CircuitBreaker::new(1, Duration::from_millis(10))
.with_config_override(|c| c.success_threshold = 2);
cb.register("s1");
cb.record_failure("s1");
thread::sleep(Duration::from_millis(15));
assert_eq!(cb.state("s1"), CircuitState::HalfOpen);
cb.record_success("s1");
assert_eq!(cb.state("s1"), CircuitState::HalfOpen);
cb.record_success("s1");
assert_eq!(cb.state("s1"), CircuitState::Closed);
}
#[test]
fn test_half_open_failure_reopens_circuit() {
let cb = CircuitBreaker::new(1, Duration::from_millis(10));
cb.register("s1");
cb.record_failure("s1");
thread::sleep(Duration::from_millis(15));
assert_eq!(cb.state("s1"), CircuitState::HalfOpen);
cb.record_failure("s1");
assert_eq!(cb.state("s1"), CircuitState::Open);
}
#[test]
fn test_closed_success_resets_failure_count() {
let cb = CircuitBreaker::new(3, Duration::from_secs(30));
cb.register("s1");
cb.record_failure("s1");
cb.record_failure("s1");
assert_eq!(cb.failure_count("s1"), 2);
cb.record_success("s1");
assert_eq!(cb.failure_count("s1"), 0);
}
#[test]
fn test_manual_reset() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.record_failure("s1");
assert_eq!(cb.state("s1"), CircuitState::Open);
cb.reset("s1");
assert_eq!(cb.state("s1"), CircuitState::Closed);
assert!(cb.allow_request("s1"));
}
#[test]
fn test_force_open() {
let cb = CircuitBreaker::new(10, Duration::from_secs(60));
cb.register("s1");
assert_eq!(cb.state("s1"), CircuitState::Closed);
cb.force_open("s1");
assert_eq!(cb.state("s1"), CircuitState::Open);
assert!(!cb.allow_request("s1"));
}
#[test]
fn test_total_and_rejected_counts() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.record_failure("s1");
assert!(!cb.allow_request("s1"));
assert!(!cb.allow_request("s1"));
assert!(!cb.allow_request("s1"));
assert_eq!(cb.total_requests(), 3);
assert_eq!(cb.rejected_requests(), 3);
}
#[test]
fn test_rejection_rate() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
assert_eq!(cb.rejection_rate(), 0.0);
cb.allow_request("s1");
cb.record_failure("s1");
cb.allow_request("s1");
let rate = cb.rejection_rate();
assert!(
(0.4..=0.6).contains(&rate),
"rejection rate should be ~0.5, got {rate}"
);
}
#[test]
fn test_list_by_state() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.register("s2");
cb.register("s3");
cb.record_failure("s1");
cb.force_open("s3");
let mut closed = cb.closed_slaves();
closed.sort();
assert_eq!(closed, vec!["s2".to_string()]);
let mut open = cb.open_slaves();
open.sort();
assert_eq!(open, vec!["s1".to_string(), "s3".to_string()]);
}
#[test]
fn test_config_builder() {
let config = CircuitBreakerConfig::new(10, Duration::from_secs(60))
.with_half_open_max_calls(5)
.with_success_threshold(3);
assert_eq!(config.failure_threshold, 10);
assert_eq!(config.half_open_max_calls, 5);
assert_eq!(config.success_threshold, 3);
}
#[test]
fn test_success_count_in_half_open() {
let cb = CircuitBreaker::new(1, Duration::from_millis(10))
.with_config_override(|c| c.success_threshold = 3);
cb.register("s1");
cb.record_failure("s1");
thread::sleep(Duration::from_millis(15));
cb.record_success("s1");
assert_eq!(cb.success_count("s1"), 1);
cb.record_success("s1");
assert_eq!(cb.success_count("s1"), 2);
}
#[test]
fn test_half_open_slaves() {
let cb = CircuitBreaker::new(1, Duration::from_millis(10));
cb.register("s1");
cb.register("s2");
cb.record_failure("s1");
thread::sleep(Duration::from_millis(15));
assert_eq!(cb.half_open_slaves(), vec!["s1".to_string()]);
}
#[test]
fn test_registered_slaves() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.register("s2");
let mut slaves = cb.registered_slaves();
slaves.sort();
assert_eq!(slaves, vec!["s1".to_string(), "s2".to_string()]);
}
#[test]
fn test_slave_count() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.register("s2");
assert_eq!(cb.slave_count(), 2);
}
#[test]
fn test_health_score_all_closed() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.register("s2");
assert_eq!(cb.health_score(), 1.0);
}
#[test]
fn test_health_score_half_open() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.register("s2");
cb.record_failure("s1");
assert!((0.4..=0.6).contains(&cb.health_score()));
}
#[test]
fn test_health_score_empty() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
assert_eq!(cb.health_score(), 1.0);
}
#[test]
fn test_summary_contains_info() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
cb.register("s1");
cb.record_failure("s1");
let s = cb.summary();
assert!(s.contains("s1"));
assert!(s.contains("open"));
}
#[test]
fn test_rejection_rate_no_requests() {
let cb = CircuitBreaker::new(1, Duration::from_secs(60));
assert_eq!(cb.rejection_rate(), 0.0);
}
#[test]
fn test_force_open_then_reset() {
let cb = CircuitBreaker::new(10, Duration::from_secs(60));
cb.register("s1");
cb.force_open("s1");
assert!(!cb.allow_request("s1"));
cb.reset("s1");
assert!(cb.allow_request("s1"));
}
trait CircuitBreakerConfigOverride: Sized {
fn with_config_override<F>(self, f: F) -> Self
where
F: FnOnce(&mut CircuitBreakerConfig);
}
impl CircuitBreakerConfigOverride for CircuitBreaker {
fn with_config_override<F>(self, f: F) -> Self
where
F: FnOnce(&mut CircuitBreakerConfig),
{
let mut config = self.config.clone();
f(&mut config);
Self::with_config(config)
}
}
}