use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use parking_lot::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakerState {
Closed,
Open,
HalfOpen,
}
impl BreakerState {
pub fn allows_requests(&self) -> bool {
matches!(self, Self::Closed | Self::HalfOpen)
}
pub fn is_tripped(&self) -> bool {
matches!(self, Self::Open | Self::HalfOpen)
}
}
#[derive(Debug, Clone)]
pub struct BreakerConfig {
pub failure_threshold: u32,
pub success_threshold: u32,
pub reset_timeout: Duration,
pub failure_window: Duration,
pub half_open_max_requests: u32,
}
impl Default for BreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
success_threshold: 3,
reset_timeout: Duration::from_secs(30),
failure_window: Duration::from_secs(60),
half_open_max_requests: 3,
}
}
}
impl BreakerConfig {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_failure_threshold(mut self, threshold: u32) -> Self {
self.failure_threshold = threshold;
self
}
#[must_use]
pub fn with_success_threshold(mut self, threshold: u32) -> Self {
self.success_threshold = threshold;
self
}
#[must_use]
pub fn with_reset_timeout(mut self, timeout: Duration) -> Self {
self.reset_timeout = timeout;
self
}
pub fn sensitive() -> Self {
Self {
failure_threshold: 3,
success_threshold: 2,
reset_timeout: Duration::from_secs(15),
failure_window: Duration::from_secs(30),
half_open_max_requests: 1,
}
}
pub fn tolerant() -> Self {
Self {
failure_threshold: 10,
success_threshold: 5,
reset_timeout: Duration::from_secs(60),
failure_window: Duration::from_secs(120),
half_open_max_requests: 5,
}
}
}
#[derive(Debug)]
pub struct CircuitBreaker {
config: BreakerConfig,
state: RwLock<BreakerState>,
failure_count: AtomicU32,
success_count: AtomicU32,
half_open_requests: AtomicU32,
last_failure: RwLock<Option<Instant>>,
opened_at: RwLock<Option<Instant>>,
total_requests: AtomicU64,
total_failures: AtomicU64,
total_rejections: AtomicU64,
}
impl CircuitBreaker {
pub fn new(config: BreakerConfig) -> Self {
Self {
config,
state: RwLock::new(BreakerState::Closed),
failure_count: AtomicU32::new(0),
success_count: AtomicU32::new(0),
half_open_requests: AtomicU32::new(0),
last_failure: RwLock::new(None),
opened_at: RwLock::new(None),
total_requests: AtomicU64::new(0),
total_failures: AtomicU64::new(0),
total_rejections: AtomicU64::new(0),
}
}
pub fn state(&self) -> BreakerState {
self.maybe_transition();
*self.state.read()
}
pub fn allow_request(&self) -> BreakerResult {
self.total_requests.fetch_add(1, Ordering::Relaxed);
self.maybe_transition();
let state = *self.state.read();
match state {
BreakerState::Closed => BreakerResult::Allowed,
BreakerState::Open => {
self.total_rejections.fetch_add(1, Ordering::Relaxed);
BreakerResult::Rejected {
retry_after: self.time_until_half_open(),
}
}
BreakerState::HalfOpen => {
let current = self.half_open_requests.fetch_add(1, Ordering::Relaxed);
if current < self.config.half_open_max_requests {
BreakerResult::AllowedProbe
} else {
self.half_open_requests.fetch_sub(1, Ordering::Relaxed);
self.total_rejections.fetch_add(1, Ordering::Relaxed);
BreakerResult::Rejected {
retry_after: Some(Duration::from_millis(100)),
}
}
}
}
}
pub fn record_success(&self) {
let state = *self.state.read();
match state {
BreakerState::Closed => {
self.failure_count.store(0, Ordering::Relaxed);
}
BreakerState::HalfOpen => {
self.half_open_requests.fetch_sub(1, Ordering::Relaxed);
let count = self.success_count.fetch_add(1, Ordering::Relaxed) + 1;
if count >= self.config.success_threshold {
self.close();
}
}
BreakerState::Open => {
}
}
}
pub fn record_failure(&self) {
self.total_failures.fetch_add(1, Ordering::Relaxed);
*self.last_failure.write() = Some(Instant::now());
let state = *self.state.read();
match state {
BreakerState::Closed => {
let count = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
if count >= self.config.failure_threshold {
self.open();
}
}
BreakerState::HalfOpen => {
self.half_open_requests.fetch_sub(1, Ordering::Relaxed);
self.open();
}
BreakerState::Open => {
}
}
}
fn open(&self) {
let mut state = self.state.write();
*state = BreakerState::Open;
*self.opened_at.write() = Some(Instant::now());
self.success_count.store(0, Ordering::Relaxed);
self.half_open_requests.store(0, Ordering::Relaxed);
}
fn close(&self) {
let mut state = self.state.write();
*state = BreakerState::Closed;
self.failure_count.store(0, Ordering::Relaxed);
self.success_count.store(0, Ordering::Relaxed);
*self.opened_at.write() = None;
}
fn maybe_transition(&self) {
let current_state = *self.state.read();
match current_state {
BreakerState::Open => {
if let Some(opened) = *self.opened_at.read() {
if opened.elapsed() >= self.config.reset_timeout {
let mut state = self.state.write();
if *state == BreakerState::Open {
*state = BreakerState::HalfOpen;
self.half_open_requests.store(0, Ordering::Relaxed);
self.success_count.store(0, Ordering::Relaxed);
}
}
}
}
BreakerState::Closed => {
if let Some(last) = *self.last_failure.read() {
if last.elapsed() > self.config.failure_window {
self.failure_count.store(0, Ordering::Relaxed);
}
}
}
BreakerState::HalfOpen => {
}
}
}
fn time_until_half_open(&self) -> Option<Duration> {
if let Some(opened) = *self.opened_at.read() {
let elapsed = opened.elapsed();
if elapsed < self.config.reset_timeout {
return Some(self.config.reset_timeout - elapsed);
}
}
None
}
pub fn force_close(&self) {
self.close();
}
pub fn force_open(&self) {
self.open();
}
pub fn stats(&self) -> BreakerStats {
BreakerStats {
state: self.state(),
total_requests: self.total_requests.load(Ordering::Relaxed),
total_failures: self.total_failures.load(Ordering::Relaxed),
total_rejections: self.total_rejections.load(Ordering::Relaxed),
current_failures: self.failure_count.load(Ordering::Relaxed),
current_successes: self.success_count.load(Ordering::Relaxed),
}
}
}
impl Default for CircuitBreaker {
fn default() -> Self {
Self::new(BreakerConfig::default())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BreakerResult {
Allowed,
AllowedProbe,
Rejected {
retry_after: Option<Duration>,
},
}
impl BreakerResult {
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Allowed | Self::AllowedProbe)
}
pub fn is_probe(&self) -> bool {
matches!(self, Self::AllowedProbe)
}
}
#[derive(Debug, Clone)]
pub struct BreakerStats {
pub state: BreakerState,
pub total_requests: u64,
pub total_failures: u64,
pub total_rejections: u64,
pub current_failures: u32,
pub current_successes: u32,
}
impl BreakerStats {
pub fn failure_rate(&self) -> f64 {
if self.total_requests == 0 {
return 0.0;
}
self.total_failures as f64 / self.total_requests as f64
}
pub fn rejection_rate(&self) -> f64 {
if self.total_requests == 0 {
return 0.0;
}
self.total_rejections as f64 / self.total_requests as f64
}
}
#[derive(Debug)]
pub struct BreakerManager {
config: BreakerConfig,
breakers: RwLock<std::collections::HashMap<String, Arc<CircuitBreaker>>>,
}
impl BreakerManager {
pub fn new(config: BreakerConfig) -> Self {
Self {
config,
breakers: RwLock::new(std::collections::HashMap::new()),
}
}
pub fn get(&self, host: &str) -> Arc<CircuitBreaker> {
{
let breakers = self.breakers.read();
if let Some(breaker) = breakers.get(host) {
return Arc::clone(breaker);
}
}
let mut breakers = self.breakers.write();
breakers
.entry(host.to_string())
.or_insert_with(|| Arc::new(CircuitBreaker::new(self.config.clone())))
.clone()
}
pub fn allow_request(&self, host: &str) -> BreakerResult {
self.get(host).allow_request()
}
pub fn record_success(&self, host: &str) {
self.get(host).record_success();
}
pub fn record_failure(&self, host: &str) {
self.get(host).record_failure();
}
pub fn open_circuits(&self) -> Vec<String> {
let breakers = self.breakers.read();
breakers
.iter()
.filter(|(_, b)| b.state() == BreakerState::Open)
.map(|(h, _)| h.clone())
.collect()
}
pub fn all_stats(&self) -> Vec<(String, BreakerStats)> {
let breakers = self.breakers.read();
breakers
.iter()
.map(|(h, b)| (h.clone(), b.stats()))
.collect()
}
}
impl Default for BreakerManager {
fn default() -> Self {
Self::new(BreakerConfig::default())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_circuit_breaker_closed() {
let breaker = CircuitBreaker::default();
assert_eq!(breaker.state(), BreakerState::Closed);
assert!(breaker.allow_request().is_allowed());
}
#[test]
fn test_circuit_breaker_opens_on_failures() {
let config = BreakerConfig::new().with_failure_threshold(3);
let breaker = CircuitBreaker::new(config);
breaker.record_failure();
breaker.record_failure();
assert_eq!(breaker.state(), BreakerState::Closed);
breaker.record_failure();
assert_eq!(breaker.state(), BreakerState::Open);
assert!(!breaker.allow_request().is_allowed());
}
#[test]
fn test_circuit_breaker_success_resets_failures() {
let config = BreakerConfig::new().with_failure_threshold(3);
let breaker = CircuitBreaker::new(config);
breaker.record_failure();
breaker.record_failure();
breaker.record_success();
breaker.record_failure();
assert_eq!(breaker.state(), BreakerState::Closed);
}
#[test]
fn test_circuit_breaker_half_open() {
let config = BreakerConfig::new()
.with_failure_threshold(1)
.with_reset_timeout(Duration::from_millis(10));
let breaker = CircuitBreaker::new(config);
breaker.record_failure();
assert_eq!(breaker.state(), BreakerState::Open);
std::thread::sleep(Duration::from_millis(15));
assert_eq!(breaker.state(), BreakerState::HalfOpen);
}
#[test]
fn test_breaker_manager() {
let manager = BreakerManager::default();
manager.record_success("host1.onion");
manager.record_failure("host2.onion");
let stats1 = manager.get("host1.onion").stats();
let stats2 = manager.get("host2.onion").stats();
assert_eq!(stats1.total_failures, 0);
assert_eq!(stats2.total_failures, 1);
}
#[test]
fn test_breaker_stats() {
let breaker = CircuitBreaker::default();
for _ in 0..10 {
let _ = breaker.allow_request();
breaker.record_success();
}
for _ in 0..2 {
let _ = breaker.allow_request();
breaker.record_failure();
}
let stats = breaker.stats();
assert_eq!(stats.total_requests, 12);
assert!(stats.failure_rate() > 0.16 && stats.failure_rate() < 0.17);
}
}