use crate::backend_stats::BackendStats;
use dashmap::DashMap;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub struct CircuitBreakerConfig {
pub max_consecutive_failures: u32,
pub max_failure_percent: f64,
pub min_requests_threshold: u64,
pub half_open_consecutive_success_threshold: u32,
pub open_duration: Duration,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
max_consecutive_failures: 5, max_failure_percent: 50.0, min_requests_threshold: 10, half_open_consecutive_success_threshold: 5,
open_duration: Duration::from_secs(10),
}
}
}
const STATE_CLOSED: u8 = 0;
const STATE_OPEN: u8 = 1;
const STATE_HALF_OPEN: u8 = 2;
struct BreakerStateData {
open_until: Instant,
probes_sent: u32,
}
impl BreakerStateData {
fn new(open_duration: Duration) -> Self {
Self {
open_until: Instant::now() + open_duration,
probes_sent: 0,
}
}
}
struct BackendCircuitState {
current_state: AtomicU8,
data: Mutex<BreakerStateData>,
}
impl BackendCircuitState {
fn new() -> Self {
Self {
current_state: AtomicU8::new(STATE_CLOSED),
data: Mutex::new(BreakerStateData {
open_until: Instant::now(),
probes_sent: 0,
}),
}
}
}
pub struct BackendCircuitStates {
backend_states: DashMap<String, Arc<BackendCircuitState>>,
config: CircuitBreakerConfig,
}
impl BackendCircuitStates {
pub fn new(config: CircuitBreakerConfig) -> Self {
Self {
backend_states: DashMap::new(),
config,
}
}
fn get_or_create_backend_circuit_state(
&self,
address: &str,
) -> Arc<BackendCircuitState> {
self.backend_states
.entry(address.to_string())
.or_insert_with(|| Arc::new(BackendCircuitState::new()))
.clone()
}
fn check_open_state_non_blocking(
&self,
state: &BackendCircuitState,
) -> bool {
let Ok(mut data) = state.data.try_lock() else {
return false;
};
if Instant::now() >= data.open_until {
data.probes_sent = 1; state
.current_state
.store(STATE_HALF_OPEN, Ordering::Relaxed);
true } else {
false }
}
fn check_half_open_state_non_blocking(
&self,
state: &BackendCircuitState,
) -> bool {
let Ok(mut data) = state.data.try_lock() else {
return false; };
if data.probes_sent
< self.config.half_open_consecutive_success_threshold
{
data.probes_sent += 1;
true } else {
false }
}
pub fn is_backend_acceptable(&self, address: &str) -> bool {
let state = self.get_or_create_backend_circuit_state(address);
let state_now = state.current_state.load(Ordering::Relaxed);
match state_now {
STATE_CLOSED => {
true
},
STATE_OPEN => {
self.check_open_state_non_blocking(&state)
},
STATE_HALF_OPEN => {
self.check_half_open_state_non_blocking(&state)
},
_ => false, }
}
pub fn update_state_after_request(
&self,
address: &str,
is_request_failure: bool,
stats: &BackendStats,
) {
let state = self.get_or_create_backend_circuit_state(address);
if !is_request_failure {
let state_now = state.current_state.load(Ordering::Relaxed);
if state_now == STATE_CLOSED {
return;
}
if state_now == STATE_HALF_OPEN {
if stats.get_consecutive_successes(address)
>= self.config.half_open_consecutive_success_threshold
{
state.current_state.store(STATE_CLOSED, Ordering::Relaxed);
}
}
} else {
let state_now = state.current_state.load(Ordering::Relaxed);
let Ok(mut data) = state.data.try_lock() else {
return;
};
match state_now {
STATE_CLOSED => {
let mut is_fail = false;
let config = &self.config;
if config.max_consecutive_failures > 0
&& stats.get_consecutive_failures(address)
>= config.max_consecutive_failures
{
is_fail = true
}
if !is_fail && config.max_failure_percent <= 100.0 {
let stats = stats.get_window_stats(address);
if stats.total_requests >= config.min_requests_threshold
&& stats.failure_rate_percent
>= config.max_failure_percent
{
is_fail = true
}
}
if is_fail {
state
.current_state
.store(STATE_HALF_OPEN, Ordering::Relaxed);
*data =
BreakerStateData::new(self.config.open_duration);
}
},
STATE_HALF_OPEN => {
state.current_state.store(STATE_OPEN, Ordering::Relaxed);
*data = BreakerStateData::new(self.config.open_duration);
},
_ => {}, }
}
}
}