use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use parking_lot::RwLock;
use tracing::{debug, info};
use crate::isolation::IsolationToken;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CircuitId(u64);
static CIRCUIT_COUNTER: AtomicU64 = AtomicU64::new(0);
impl CircuitId {
pub fn new() -> Self {
Self(CIRCUIT_COUNTER.fetch_add(1, Ordering::Relaxed))
}
}
impl Default for CircuitId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitHealth {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Debug)]
pub struct CircuitInfo {
pub id: CircuitId,
pub created_at: Instant,
pub last_used: Instant,
pub request_count: u64,
pub failure_count: u64,
pub isolation_token: Option<IsolationToken>,
}
impl CircuitInfo {
pub fn new(isolation_token: Option<IsolationToken>) -> Self {
let now = Instant::now();
Self {
id: CircuitId::new(),
created_at: now,
last_used: now,
request_count: 0,
failure_count: 0,
isolation_token,
}
}
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
pub fn idle_time(&self) -> Duration {
self.last_used.elapsed()
}
pub fn record_success(&mut self) {
self.last_used = Instant::now();
self.request_count += 1;
}
pub fn record_failure(&mut self) {
self.last_used = Instant::now();
self.failure_count += 1;
}
pub fn health(&self, config: &CircuitConfig) -> CircuitHealth {
if self.age() > config.max_circuit_age {
return CircuitHealth::Unhealthy;
}
if self.failure_count > config.max_failures {
return CircuitHealth::Unhealthy;
}
if self.request_count > config.max_requests_per_circuit {
return CircuitHealth::Unhealthy;
}
if self.request_count > 10 {
let failure_rate = self.failure_count as f64 / self.request_count as f64;
if failure_rate > 0.3 {
return CircuitHealth::Degraded;
}
}
CircuitHealth::Healthy
}
}
#[derive(Debug, Clone)]
pub struct CircuitConfig {
pub max_circuit_age: Duration,
pub max_requests_per_circuit: u64,
pub max_failures: u64,
pub idle_timeout: Duration,
}
impl Default for CircuitConfig {
fn default() -> Self {
Self {
max_circuit_age: Duration::from_secs(600), max_requests_per_circuit: 100, max_failures: 5, idle_timeout: Duration::from_secs(120), }
}
}
pub struct CircuitManager {
circuits: RwLock<HashMap<String, CircuitInfo>>,
config: CircuitConfig,
}
impl CircuitManager {
pub fn new(config: CircuitConfig) -> Self {
Self {
circuits: RwLock::new(HashMap::new()),
config,
}
}
pub fn get_or_create(&self, destination: &str, isolation: Option<IsolationToken>) -> CircuitId {
let key = self.make_key(destination, isolation);
{
let circuits = self.circuits.read();
if let Some(info) = circuits.get(&key) {
if info.health(&self.config) == CircuitHealth::Healthy {
return info.id;
}
}
}
let mut circuits = self.circuits.write();
let info = CircuitInfo::new(isolation);
let id = info.id;
circuits.insert(key, info);
debug!(circuit_id = id.0, destination, "Created new circuit");
id
}
pub fn record_success(&self, destination: &str, isolation: Option<IsolationToken>) {
let key = self.make_key(destination, isolation);
let mut circuits = self.circuits.write();
if let Some(info) = circuits.get_mut(&key) {
info.record_success();
}
}
pub fn record_failure(&self, destination: &str, isolation: Option<IsolationToken>) {
let key = self.make_key(destination, isolation);
let mut circuits = self.circuits.write();
if let Some(info) = circuits.get_mut(&key) {
info.record_failure();
}
}
pub fn cleanup(&self) {
let mut circuits = self.circuits.write();
let before = circuits.len();
circuits.retain(|_, info| {
info.health(&self.config) != CircuitHealth::Unhealthy
&& info.idle_time() < self.config.idle_timeout
});
let removed = before - circuits.len();
if removed > 0 {
info!(removed, remaining = circuits.len(), "Cleaned up circuits");
}
}
pub fn stats(&self) -> CircuitStats {
let circuits = self.circuits.read();
let mut healthy = 0;
let mut degraded = 0;
let mut unhealthy = 0;
for info in circuits.values() {
match info.health(&self.config) {
CircuitHealth::Healthy => healthy += 1,
CircuitHealth::Degraded => degraded += 1,
CircuitHealth::Unhealthy => unhealthy += 1,
}
}
CircuitStats {
total: circuits.len(),
healthy,
degraded,
unhealthy,
}
}
fn make_key(&self, destination: &str, isolation: Option<IsolationToken>) -> String {
match isolation {
Some(token) => format!("{}:{}", destination, token.as_raw()),
None => destination.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct CircuitStats {
pub total: usize,
pub healthy: usize,
pub degraded: usize,
pub unhealthy: usize,
}
impl Default for CircuitManager {
fn default() -> Self {
Self::new(CircuitConfig::default())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn test_circuit_id_uniqueness() {
let id1 = CircuitId::new();
let id2 = CircuitId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_circuit_health_new() {
let config = CircuitConfig::default();
let info = CircuitInfo::new(None);
assert_eq!(info.health(&config), CircuitHealth::Healthy);
}
#[test]
fn test_circuit_manager_get_or_create() {
let manager = CircuitManager::default();
let id1 = manager.get_or_create("example.com:443", None);
let id2 = manager.get_or_create("example.com:443", None);
assert_eq!(id1, id2); }
}