use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug)]
pub struct CircuitBreaker {
failure_count: AtomicU32,
last_failure_time: AtomicU64,
threshold: u32,
reset_seconds: u64,
}
impl CircuitBreaker {
#[must_use]
pub fn new(threshold: u32, reset_seconds: u64) -> Self {
Self {
failure_count: AtomicU32::new(0),
last_failure_time: AtomicU64::new(0),
threshold,
reset_seconds,
}
}
#[must_use]
pub fn is_open(&self) -> bool {
let failures = self.failure_count.load(Ordering::Relaxed);
if failures < self.threshold {
return false;
}
let last_failure = self.last_failure_time.load(Ordering::Relaxed);
let now = current_time_secs();
now < last_failure + self.reset_seconds
}
pub fn record_success(&self) {
self.failure_count.store(0, Ordering::Relaxed);
}
pub fn record_failure(&self) {
let new_count = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
if new_count >= self.threshold {
let now = current_time_secs();
self.last_failure_time.store(now, Ordering::Relaxed);
}
}
}
#[cfg(test)]
impl CircuitBreaker {
#[must_use]
pub fn failure_count(&self) -> u32 {
self.failure_count.load(Ordering::Relaxed)
}
#[must_use]
pub fn last_failure_time(&self) -> u64 {
self.last_failure_time.load(Ordering::Relaxed)
}
}
fn current_time_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_circuit_closed_initially() {
let cb = CircuitBreaker::new(3, 60);
assert!(!cb.is_open());
assert_eq!(cb.failure_count(), 0);
}
#[test]
fn test_circuit_opens_after_threshold() {
let cb = CircuitBreaker::new(3, 60);
cb.record_failure();
assert!(!cb.is_open());
cb.record_failure();
assert!(!cb.is_open());
cb.record_failure();
assert!(cb.is_open());
assert_eq!(cb.failure_count(), 3);
}
#[test]
fn test_circuit_closes_on_success() {
let cb = CircuitBreaker::new(3, 60);
cb.record_failure();
cb.record_failure();
cb.record_failure();
assert!(cb.is_open());
cb.record_success();
assert!(!cb.is_open());
assert_eq!(cb.failure_count(), 0);
}
#[test]
fn test_circuit_half_open_after_reset_timeout() {
let cb = CircuitBreaker::new(2, 1);
cb.record_failure();
cb.record_failure();
assert!(cb.is_open());
thread::sleep(Duration::from_secs(2));
assert!(!cb.is_open());
}
#[test]
fn test_circuit_reopens_on_failure_in_half_open() {
let cb = CircuitBreaker::new(2, 1);
cb.record_failure();
cb.record_failure();
assert!(cb.is_open());
thread::sleep(Duration::from_secs(2));
assert!(!cb.is_open());
cb.record_failure();
assert!(cb.is_open());
}
#[test]
fn test_custom_threshold_and_reset() {
let cb = CircuitBreaker::new(5, 120);
for _ in 0..4 {
cb.record_failure();
}
assert!(!cb.is_open());
cb.record_failure();
assert!(cb.is_open());
}
}