use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
use std::time::Instant;
use tracing::{debug, info, trace, warn};
use crate::types::{CircuitBreakerConfig, CircuitBreakerState};
const STATE_CLOSED: u8 = 0;
const STATE_OPEN: u8 = 1;
const STATE_HALF_OPEN: u8 = 2;
pub struct CircuitBreaker {
config: CircuitBreakerConfig,
state: AtomicU8,
consecutive_failures: AtomicU64,
consecutive_successes: AtomicU64,
base_instant: Instant,
last_state_change_ns: AtomicU64,
half_open_requests: AtomicU64,
name: Option<String>,
}
impl CircuitBreaker {
pub fn new(config: CircuitBreakerConfig) -> Self {
trace!(
failure_threshold = config.failure_threshold,
success_threshold = config.success_threshold,
timeout_seconds = config.timeout_seconds,
half_open_max_requests = config.half_open_max_requests,
"Creating circuit breaker"
);
let now = Instant::now();
Self {
config,
state: AtomicU8::new(STATE_CLOSED),
consecutive_failures: AtomicU64::new(0),
consecutive_successes: AtomicU64::new(0),
base_instant: now,
last_state_change_ns: AtomicU64::new(0),
half_open_requests: AtomicU64::new(0),
name: None,
}
}
pub fn with_name(config: CircuitBreakerConfig, name: impl Into<String>) -> Self {
let name = name.into();
debug!(
name = %name,
failure_threshold = config.failure_threshold,
success_threshold = config.success_threshold,
timeout_seconds = config.timeout_seconds,
"Creating named circuit breaker"
);
let now = Instant::now();
Self {
config,
state: AtomicU8::new(STATE_CLOSED),
consecutive_failures: AtomicU64::new(0),
consecutive_successes: AtomicU64::new(0),
base_instant: now,
last_state_change_ns: AtomicU64::new(0),
half_open_requests: AtomicU64::new(0),
name: Some(name),
}
}
#[inline]
pub fn is_closed(&self) -> bool {
let state = self.state.load(Ordering::Acquire);
match state {
STATE_CLOSED => {
trace!(name = ?self.name, state = "closed", "Circuit breaker check: allowed");
true
}
STATE_OPEN => {
let last_change_ns = self.last_state_change_ns.load(Ordering::Acquire);
let current_ns = self.base_instant.elapsed().as_nanos() as u64;
let elapsed_ns = current_ns.saturating_sub(last_change_ns);
let timeout_ns = self.config.timeout_seconds * 1_000_000_000;
if elapsed_ns >= timeout_ns {
trace!(
name = ?self.name,
elapsed_secs = elapsed_ns / 1_000_000_000,
"Open timeout reached, transitioning to half-open"
);
self.transition_to_half_open();
true } else {
trace!(
name = ?self.name,
state = "open",
remaining_secs = (timeout_ns - elapsed_ns) / 1_000_000_000,
"Circuit breaker check: blocked"
);
false
}
}
STATE_HALF_OPEN => {
let current = self.half_open_requests.fetch_add(1, Ordering::Relaxed);
let allowed = current < self.config.half_open_max_requests.into();
trace!(
name = ?self.name,
state = "half-open",
request_num = current + 1,
max_requests = self.config.half_open_max_requests,
allowed = allowed,
"Circuit breaker half-open check"
);
allowed
}
_ => {
true
}
}
}
#[inline]
pub async fn is_closed_async(&self) -> bool {
self.is_closed()
}
#[inline]
pub fn record_success(&self) {
self.consecutive_failures.store(0, Ordering::Relaxed);
let successes = self.consecutive_successes.fetch_add(1, Ordering::Relaxed) + 1;
trace!(
name = ?self.name,
consecutive_successes = successes,
success_threshold = self.config.success_threshold,
"Recorded success"
);
let state = self.state.load(Ordering::Acquire);
if state == STATE_HALF_OPEN && successes >= self.config.success_threshold.into() {
self.transition_to_closed();
}
}
#[inline]
pub async fn record_success_async(&self) {
self.record_success()
}
#[inline]
pub fn record_failure(&self) -> bool {
self.consecutive_successes.store(0, Ordering::Relaxed);
let failures = self.consecutive_failures.fetch_add(1, Ordering::Relaxed) + 1;
trace!(
name = ?self.name,
consecutive_failures = failures,
failure_threshold = self.config.failure_threshold,
"Recorded failure"
);
let state = self.state.load(Ordering::Acquire);
match state {
STATE_CLOSED if failures >= self.config.failure_threshold.into() => {
self.transition_to_open();
true
}
STATE_HALF_OPEN => {
debug!(
name = ?self.name,
"Failure in half-open state, re-opening circuit"
);
self.transition_to_open();
true
}
_ => false,
}
}
#[inline]
pub async fn record_failure_async(&self) -> bool {
self.record_failure()
}
#[inline]
pub fn state(&self) -> CircuitBreakerState {
match self.state.load(Ordering::Acquire) {
STATE_CLOSED => CircuitBreakerState::Closed,
STATE_OPEN => CircuitBreakerState::Open,
STATE_HALF_OPEN => CircuitBreakerState::HalfOpen,
_ => CircuitBreakerState::Closed, }
}
#[inline]
pub async fn state_async(&self) -> CircuitBreakerState {
self.state()
}
#[inline]
pub fn consecutive_failures(&self) -> u64 {
self.consecutive_failures.load(Ordering::Relaxed)
}
#[inline]
pub fn consecutive_successes(&self) -> u64 {
self.consecutive_successes.load(Ordering::Relaxed)
}
pub fn reset(&self) {
self.state.store(STATE_CLOSED, Ordering::Release);
self.last_state_change_ns.store(
self.base_instant.elapsed().as_nanos() as u64,
Ordering::Release,
);
self.consecutive_failures.store(0, Ordering::Relaxed);
self.consecutive_successes.store(0, Ordering::Relaxed);
self.half_open_requests.store(0, Ordering::Relaxed);
if let Some(ref name) = self.name {
info!(name = %name, "Circuit breaker reset");
} else {
info!("Circuit breaker reset");
}
}
pub async fn reset_async(&self) {
self.reset()
}
fn transition_to_open(&self) {
let current = self.state.load(Ordering::Acquire);
if current == STATE_OPEN {
return; }
if self
.state
.compare_exchange(current, STATE_OPEN, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
self.last_state_change_ns.store(
self.base_instant.elapsed().as_nanos() as u64,
Ordering::Release,
);
if let Some(ref name) = self.name {
warn!(name = %name, "Circuit breaker opened");
} else {
warn!("Circuit breaker opened");
}
}
}
fn transition_to_closed(&self) {
let current = self.state.load(Ordering::Acquire);
if current == STATE_CLOSED {
return; }
if self
.state
.compare_exchange(current, STATE_CLOSED, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
self.last_state_change_ns.store(
self.base_instant.elapsed().as_nanos() as u64,
Ordering::Release,
);
self.consecutive_failures.store(0, Ordering::Relaxed);
self.consecutive_successes.store(0, Ordering::Relaxed);
self.half_open_requests.store(0, Ordering::Relaxed);
if let Some(ref name) = self.name {
info!(name = %name, "Circuit breaker closed");
} else {
info!("Circuit breaker closed");
}
}
}
fn transition_to_half_open(&self) {
let current = self.state.load(Ordering::Acquire);
if current == STATE_HALF_OPEN {
return; }
if current != STATE_OPEN {
return;
}
if self
.state
.compare_exchange(
STATE_OPEN,
STATE_HALF_OPEN,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
self.last_state_change_ns.store(
self.base_instant.elapsed().as_nanos() as u64,
Ordering::Release,
);
self.half_open_requests.store(0, Ordering::Relaxed);
if let Some(ref name) = self.name {
info!(name = %name, "Circuit breaker half-open");
} else {
info!("Circuit breaker half-open");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn test_config() -> CircuitBreakerConfig {
CircuitBreakerConfig {
failure_threshold: 3,
success_threshold: 2,
timeout_seconds: 1,
half_open_max_requests: 2,
}
}
#[test]
fn test_initial_state_is_closed() {
let cb = CircuitBreaker::new(test_config());
assert!(cb.is_closed());
assert_eq!(cb.state(), CircuitBreakerState::Closed);
}
#[test]
fn test_opens_after_failure_threshold() {
let cb = CircuitBreaker::new(test_config());
for _ in 0..3 {
cb.record_failure();
}
assert!(!cb.is_closed());
assert_eq!(cb.state(), CircuitBreakerState::Open);
}
#[test]
fn test_success_resets_failure_count() {
let cb = CircuitBreaker::new(test_config());
cb.record_failure();
cb.record_failure();
cb.record_success();
assert!(cb.is_closed());
assert_eq!(cb.consecutive_failures(), 0);
}
#[tokio::test]
async fn test_half_open_transition() {
let config = CircuitBreakerConfig {
failure_threshold: 1,
success_threshold: 1,
timeout_seconds: 0, half_open_max_requests: 1,
};
let cb = CircuitBreaker::new(config);
cb.record_failure();
assert_eq!(cb.state(), CircuitBreakerState::Open);
tokio::time::sleep(Duration::from_millis(10)).await;
assert!(cb.is_closed()); assert_eq!(cb.state(), CircuitBreakerState::HalfOpen);
}
#[tokio::test]
async fn test_closes_after_success_threshold_in_half_open() {
let config = CircuitBreakerConfig {
failure_threshold: 1,
success_threshold: 2,
timeout_seconds: 0,
half_open_max_requests: 5,
};
let cb = CircuitBreaker::new(config);
cb.record_failure();
tokio::time::sleep(Duration::from_millis(10)).await;
cb.is_closed();
cb.record_success();
cb.record_success();
assert_eq!(cb.state(), CircuitBreakerState::Closed);
}
#[test]
fn test_named_circuit_breaker() {
let cb = CircuitBreaker::with_name(test_config(), "test-service");
assert!(cb.is_closed());
}
#[test]
fn test_reset() {
let cb = CircuitBreaker::new(test_config());
for _ in 0..3 {
cb.record_failure();
}
assert_eq!(cb.state(), CircuitBreakerState::Open);
cb.reset();
assert_eq!(cb.state(), CircuitBreakerState::Closed);
assert_eq!(cb.consecutive_failures(), 0);
}
#[test]
fn test_concurrent_access() {
use std::sync::Arc;
use std::thread;
let cb = Arc::new(CircuitBreaker::new(test_config()));
let mut handles = vec![];
for _ in 0..10 {
let cb_clone = Arc::clone(&cb);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
cb_clone.is_closed();
cb_clone.record_success();
cb_clone.record_failure();
}
}));
}
for handle in handles {
handle.join().unwrap();
}
let state = cb.state();
assert!(matches!(
state,
CircuitBreakerState::Closed | CircuitBreakerState::Open | CircuitBreakerState::HalfOpen
));
}
#[tokio::test]
async fn test_async_api_compatibility() {
let cb = CircuitBreaker::new(test_config());
assert!(cb.is_closed_async().await);
cb.record_failure_async().await;
cb.record_success_async().await;
let _ = cb.state_async().await;
cb.reset_async().await;
assert_eq!(cb.state_async().await, CircuitBreakerState::Closed);
}
}