use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
struct CircuitBreakerInner {
state: CircuitState,
failure_count: usize,
success_count: usize,
opened_at: Option<Instant>,
}
pub struct CircuitBreaker {
inner: parking_lot::Mutex<CircuitBreakerInner>,
config: CircuitBreakerConfig,
total_requests: AtomicU64,
total_failures: AtomicU64,
}
#[derive(Clone, Debug)]
pub struct CircuitBreakerConfig {
pub failure_threshold: usize,
pub success_threshold: usize,
pub timeout: Duration,
pub failure_rate_threshold: f64,
pub min_requests: usize,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
success_threshold: 3,
timeout: Duration::from_secs(30),
failure_rate_threshold: 0.5,
min_requests: 10,
}
}
}
impl CircuitBreaker {
pub fn new(config: CircuitBreakerConfig) -> Self {
Self {
inner: parking_lot::Mutex::new(CircuitBreakerInner {
state: CircuitState::Closed,
failure_count: 0,
success_count: 0,
opened_at: None,
}),
config,
total_requests: AtomicU64::new(0),
total_failures: AtomicU64::new(0),
}
}
pub fn should_allow(&self) -> bool {
let mut inner = self.inner.lock();
match inner.state {
CircuitState::Closed | CircuitState::HalfOpen => true,
CircuitState::Open => {
if let Some(opened_at) = inner.opened_at {
if opened_at.elapsed() >= self.config.timeout {
inner.state = CircuitState::HalfOpen;
inner.success_count = 0;
tracing::info!("Circuit breaker half-open");
true
} else {
false
}
} else {
false
}
}
}
}
pub fn record_success(&self) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
let mut inner = self.inner.lock();
match inner.state {
CircuitState::Closed => {
inner.failure_count = 0;
}
CircuitState::HalfOpen => {
inner.success_count += 1;
if inner.success_count >= self.config.success_threshold {
inner.state = CircuitState::Closed;
inner.opened_at = None;
inner.failure_count = 0;
inner.success_count = 0;
tracing::info!("Circuit breaker closed");
}
}
CircuitState::Open => {
}
}
}
pub fn record_failure(&self) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
self.total_failures.fetch_add(1, Ordering::Relaxed);
let mut inner = self.inner.lock();
match inner.state {
CircuitState::Closed => {
inner.failure_count += 1;
if inner.failure_count >= self.config.failure_threshold {
inner.state = CircuitState::Open;
inner.opened_at = Some(Instant::now());
inner.failure_count = 0;
inner.success_count = 0;
tracing::warn!("Circuit breaker opened");
return;
}
let total_requests = self.total_requests.load(Ordering::Relaxed);
if total_requests >= self.config.min_requests as u64 {
let total_failures = self.total_failures.load(Ordering::Relaxed);
#[allow(clippy::cast_precision_loss)]
let failure_rate = total_failures as f64 / total_requests as f64;
if failure_rate >= self.config.failure_rate_threshold {
inner.state = CircuitState::Open;
inner.opened_at = Some(Instant::now());
inner.failure_count = 0;
inner.success_count = 0;
tracing::warn!("Circuit breaker opened due to high failure rate");
}
}
}
CircuitState::HalfOpen => {
inner.state = CircuitState::Open;
inner.opened_at = Some(Instant::now());
inner.failure_count = 0;
inner.success_count = 0;
tracing::warn!("Circuit breaker reopened from half-open");
}
CircuitState::Open => {
}
}
}
pub fn state(&self) -> CircuitState {
self.inner.lock().state
}
pub fn stats(&self) -> CircuitBreakerStats {
CircuitBreakerStats {
state: self.state(),
total_requests: self.total_requests.load(Ordering::Relaxed),
total_failures: self.total_failures.load(Ordering::Relaxed),
failure_rate: {
let total = self.total_requests.load(Ordering::Relaxed);
if total > 0 {
#[allow(clippy::cast_precision_loss)]
{
self.total_failures.load(Ordering::Relaxed) as f64 / total as f64
}
} else {
0.0
}
},
}
}
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerStats {
pub state: CircuitState,
pub total_requests: u64,
pub total_failures: u64,
pub failure_rate: f64,
}
pub struct LoadShedder {
load_level: RwLock<f64>,
rejection_curve: Box<dyn Fn(f64) -> f64 + Send + Sync>,
}
impl Default for LoadShedder {
fn default() -> Self {
Self::with_curve(Box::new(|load| {
if load < 0.8 { 0.0 } else { (load - 0.8) * 5.0 }
}))
}
}
impl LoadShedder {
pub fn new() -> Self {
Self::default()
}
pub fn with_curve(curve: Box<dyn Fn(f64) -> f64 + Send + Sync>) -> Self {
Self {
load_level: RwLock::new(0.0),
rejection_curve: curve,
}
}
pub fn update_load(&self, load: f64) {
let clamped = load.clamp(0.0, 1.0);
*self.load_level.write() = clamped;
}
pub fn should_shed(&self) -> bool {
use rand::Rng;
let load = *self.load_level.read();
let rejection_prob = (self.rejection_curve)(load);
let mut rng = rand::thread_rng();
rng.gen_range(0.0..1.0) < rejection_prob
}
pub fn load(&self) -> f64 {
*self.load_level.read()
}
}
pub struct SystemHealth {
pub circuit_breaker: Arc<CircuitBreaker>,
pub load_shedder: Arc<LoadShedder>,
pub queue_threshold: usize,
queue_depth: AtomicUsize,
}
impl SystemHealth {
pub fn new(
circuit_breaker: Arc<CircuitBreaker>,
load_shedder: Arc<LoadShedder>,
queue_threshold: usize,
) -> Self {
Self {
circuit_breaker,
load_shedder,
queue_threshold,
queue_depth: AtomicUsize::new(0),
}
}
pub fn update_queue_depth(&self, depth: usize) {
self.queue_depth.store(depth, Ordering::Relaxed);
#[allow(clippy::cast_precision_loss)]
let load = depth as f64 / self.queue_threshold as f64;
self.load_shedder.update_load(load);
}
pub fn is_healthy(&self) -> bool {
if !self.circuit_breaker.should_allow() {
return false;
}
if self.load_shedder.should_shed() {
return false;
}
let depth = self.queue_depth.load(Ordering::Relaxed);
depth < self.queue_threshold
}
pub fn status(&self) -> HealthStatus {
HealthStatus {
circuit_state: self.circuit_breaker.state(),
load_level: self.load_shedder.load(),
queue_depth: self.queue_depth.load(Ordering::Relaxed),
queue_threshold: self.queue_threshold,
is_healthy: self.is_healthy(),
}
}
}
#[derive(Debug, Clone)]
pub struct HealthStatus {
pub circuit_state: CircuitState,
pub load_level: f64,
pub queue_depth: usize,
pub queue_threshold: usize,
pub is_healthy: bool,
}