use crate::core::error::{Error, Result};
use crate::lock_safe;
use scirs2_core::random::{rng, RngExt};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_attempts: u32,
pub base_delay_ms: u64,
pub max_delay_ms: u64,
pub backoff_strategy: BackoffStrategy,
pub jitter: bool,
pub backoff_multiplier: f64,
pub retryable_errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BackoffStrategy {
Fixed,
Exponential,
Linear,
Custom(Vec<u64>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircuitBreakerConfig {
pub failure_threshold: u32,
pub failure_window_seconds: u64,
pub minimum_calls: u32,
pub timeout_seconds: u64,
pub success_threshold_percentage: f64,
pub half_open_max_calls: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Debug, Clone)]
pub struct CircuitStats {
pub total_calls: u64,
pub successful_calls: u64,
pub failed_calls: u64,
pub rejected_calls: u64,
pub last_failure_time: Option<Instant>,
pub state_changed_time: Instant,
}
#[derive(Debug)]
struct Inner {
state: CircuitState,
stats: CircuitStats,
failure_times: Vec<Instant>,
call_times: Vec<Instant>,
half_open_admitted: u32,
half_open_successes: u32,
}
#[derive(Debug)]
pub struct CircuitBreaker {
config: CircuitBreakerConfig,
inner: Mutex<Inner>,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
base_delay_ms: 100,
max_delay_ms: 30_000,
backoff_strategy: BackoffStrategy::Exponential,
jitter: true,
backoff_multiplier: 2.0,
retryable_errors: vec![
"Connection error".to_string(),
"Timeout error".to_string(),
"IO error".to_string(),
"Executor error".to_string(),
],
}
}
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
failure_window_seconds: 60,
minimum_calls: 10,
timeout_seconds: 60,
success_threshold_percentage: 50.0,
half_open_max_calls: 3,
}
}
}
fn prune_window(times: &mut Vec<Instant>, now: Instant, window_seconds: u64) {
if let Some(window_start) = now.checked_sub(Duration::from_secs(window_seconds)) {
times.retain(|&t| t >= window_start);
}
}
impl CircuitBreaker {
pub fn new(config: CircuitBreakerConfig) -> Self {
Self {
config,
inner: Mutex::new(Inner {
state: CircuitState::Closed,
stats: CircuitStats {
total_calls: 0,
successful_calls: 0,
failed_calls: 0,
rejected_calls: 0,
last_failure_time: None,
state_changed_time: Instant::now(),
},
failure_times: Vec::new(),
call_times: Vec::new(),
half_open_admitted: 0,
half_open_successes: 0,
}),
}
}
pub fn can_execute(&self) -> Result<bool> {
let mut inner = lock_safe!(self.inner, "circuit breaker lock")?;
let now = Instant::now();
match inner.state {
CircuitState::Closed => Ok(true),
CircuitState::Open => {
let timeout_elapsed = now.duration_since(inner.stats.state_changed_time).as_secs()
>= self.config.timeout_seconds;
if timeout_elapsed {
inner.state = CircuitState::HalfOpen;
inner.stats.state_changed_time = now;
inner.half_open_admitted = 1;
inner.half_open_successes = 0;
Ok(true)
} else {
Ok(false)
}
}
CircuitState::HalfOpen => {
if inner.half_open_admitted < self.config.half_open_max_calls {
inner.half_open_admitted += 1;
Ok(true)
} else {
Ok(false)
}
}
}
}
pub fn record_success(&self) -> Result<()> {
let now = Instant::now();
let mut inner = lock_safe!(self.inner, "circuit breaker lock for success")?;
inner.stats.total_calls += 1;
inner.stats.successful_calls += 1;
inner.call_times.push(now);
let window = self.config.failure_window_seconds;
prune_window(&mut inner.call_times, now, window);
if inner.state == CircuitState::HalfOpen {
inner.half_open_successes += 1;
let admitted = inner.half_open_admitted.max(inner.half_open_successes);
if inner.half_open_successes >= self.config.half_open_max_calls {
let success_rate = (inner.half_open_successes as f64 / admitted as f64) * 100.0;
if success_rate >= self.config.success_threshold_percentage {
inner.state = CircuitState::Closed;
inner.stats.state_changed_time = now;
inner.failure_times.clear();
inner.call_times.clear();
inner.half_open_admitted = 0;
inner.half_open_successes = 0;
}
}
}
Ok(())
}
pub fn record_failure(&self) -> Result<()> {
let now = Instant::now();
let mut inner = lock_safe!(self.inner, "circuit breaker lock for failure")?;
inner.stats.total_calls += 1;
inner.stats.failed_calls += 1;
inner.stats.last_failure_time = Some(now);
let window = self.config.failure_window_seconds;
inner.call_times.push(now);
inner.failure_times.push(now);
prune_window(&mut inner.call_times, now, window);
prune_window(&mut inner.failure_times, now, window);
let failure_count = inner.failure_times.len() as u32;
let windowed_calls = inner.call_times.len() as u32;
match inner.state {
CircuitState::Closed => {
if windowed_calls >= self.config.minimum_calls
&& failure_count >= self.config.failure_threshold
{
inner.state = CircuitState::Open;
inner.stats.state_changed_time = now;
}
}
CircuitState::HalfOpen => {
inner.state = CircuitState::Open;
inner.stats.state_changed_time = now;
inner.half_open_admitted = 0;
inner.half_open_successes = 0;
}
CircuitState::Open => {
}
}
Ok(())
}
pub fn record_rejection(&self) -> Result<()> {
lock_safe!(self.inner, "circuit breaker lock for rejection")?
.stats
.rejected_calls += 1;
Ok(())
}
pub fn state(&self) -> Result<CircuitState> {
Ok(
lock_safe!(self.inner, "circuit breaker lock for state query")?
.state
.clone(),
)
}
pub fn stats(&self) -> Result<CircuitStats> {
Ok(
lock_safe!(self.inner, "circuit breaker lock for stats query")?
.stats
.clone(),
)
}
}
pub fn is_retryable(error: &Error) -> bool {
matches!(
error,
Error::ConnectionError(_)
| Error::TimeoutError(_)
| Error::IoError(_)
| Error::Io(_)
| Error::ExecutorError(_)
)
}
fn is_retryable_str(config: &RetryConfig, error_display: &str) -> bool {
config
.retryable_errors
.iter()
.any(|retryable| error_display.starts_with(retryable.as_str()))
}
async fn backoff_sleep(duration: Duration) {
#[cfg(feature = "resilience")]
{
tokio::time::sleep(duration).await;
}
#[cfg(not(feature = "resilience"))]
{
std::thread::sleep(duration);
}
}
#[derive(Debug)]
pub struct RetryMechanism {
config: RetryConfig,
}
impl RetryMechanism {
pub fn new(config: RetryConfig) -> Self {
Self { config }
}
pub async fn execute<F, T, E>(&self, mut operation: F) -> Result<T>
where
F: FnMut() -> std::result::Result<T, E>,
E: std::fmt::Display + std::fmt::Debug,
{
let mut attempt = 0u32;
loop {
attempt += 1;
match operation() {
Ok(result) => return Ok(result),
Err(error) => {
let error_str = format!("{}", error);
let is_retryable_now = is_retryable_str(&self.config, &error_str);
if !is_retryable_now || attempt >= self.config.max_attempts {
return Err(Error::OperationFailed(format!(
"Operation failed after {} attempts. Last error: {}",
attempt, error_str
)));
}
let delay = self.calculate_delay(attempt);
backoff_sleep(Duration::from_millis(delay)).await;
}
}
}
}
fn calculate_delay(&self, attempt: u32) -> u64 {
let base_delay = match &self.config.backoff_strategy {
BackoffStrategy::Fixed => self.config.base_delay_ms,
BackoffStrategy::Exponential => {
let exp_delay = (self.config.base_delay_ms as f64
* self.config.backoff_multiplier.powi((attempt - 1) as i32))
as u64;
std::cmp::min(exp_delay, self.config.max_delay_ms)
}
BackoffStrategy::Linear => {
let linear_delay = self.config.base_delay_ms * attempt as u64;
std::cmp::min(linear_delay, self.config.max_delay_ms)
}
BackoffStrategy::Custom(delays) => {
if attempt > 0 && (attempt as usize - 1) < delays.len() {
delays[attempt as usize - 1]
} else {
self.config.max_delay_ms
}
}
};
if self.config.jitter {
let jitter_amount = (base_delay as f64 * 0.1) as u64;
let jitter = rng().random_range(0..=jitter_amount);
base_delay + jitter
} else {
base_delay
}
}
}
#[derive(Debug)]
pub struct ResilienceManager {
circuit_breakers: Mutex<HashMap<String, std::sync::Arc<CircuitBreaker>>>,
retry_configs: Mutex<HashMap<String, RetryConfig>>,
default_retry_config: RetryConfig,
default_circuit_config: CircuitBreakerConfig,
}
impl ResilienceManager {
pub fn new() -> Self {
Self {
circuit_breakers: Mutex::new(HashMap::new()),
retry_configs: Mutex::new(HashMap::new()),
default_retry_config: RetryConfig::default(),
default_circuit_config: CircuitBreakerConfig::default(),
}
}
pub fn get_circuit_breaker(
&self,
service_name: &str,
) -> Result<std::sync::Arc<CircuitBreaker>> {
let mut breakers = lock_safe!(
self.circuit_breakers,
"resilience manager circuit breakers lock"
)?;
if !breakers.contains_key(service_name) {
let breaker =
std::sync::Arc::new(CircuitBreaker::new(self.default_circuit_config.clone()));
breakers.insert(service_name.to_string(), breaker);
}
Ok(breakers
.get(service_name)
.ok_or_else(|| {
Error::InvalidOperation(format!(
"Circuit breaker for {} should exist",
service_name
))
})?
.clone())
}
pub fn get_retry_config(&self, service_name: &str) -> Result<RetryConfig> {
let configs = lock_safe!(self.retry_configs, "resilience manager retry configs lock")?;
Ok(configs
.get(service_name)
.cloned()
.unwrap_or_else(|| self.default_retry_config.clone()))
}
pub fn set_retry_config(&self, service_name: &str, config: RetryConfig) -> Result<()> {
let mut configs = lock_safe!(
self.retry_configs,
"resilience manager retry configs lock for set"
)?;
configs.insert(service_name.to_string(), config);
Ok(())
}
pub async fn execute_with_resilience<F, T, E>(
&self,
service_name: &str,
operation: F,
) -> Result<T>
where
F: Fn() -> std::result::Result<T, E> + Send + Sync,
E: std::fmt::Display + std::fmt::Debug + Send + Sync,
{
let circuit_breaker = self.get_circuit_breaker(service_name)?;
let retry_config = self.get_retry_config(service_name)?;
let retry_mechanism = RetryMechanism::new(retry_config.clone());
let mut attempt = 0u32;
loop {
attempt += 1;
if !circuit_breaker.can_execute()? {
circuit_breaker.record_rejection()?;
return Err(Error::ConnectionError(format!(
"Circuit breaker is open for service: {}",
service_name
)));
}
match operation() {
Ok(result) => {
circuit_breaker.record_success()?;
return Ok(result);
}
Err(error) => {
circuit_breaker.record_failure()?;
let error_str = format!("{}", error);
let is_retryable_now = is_retryable_str(&retry_config, &error_str);
if !is_retryable_now || attempt >= retry_config.max_attempts {
return Err(Error::OperationFailed(format!(
"Operation failed after {} attempts for service '{}'. Last error: {}",
attempt, service_name, error_str
)));
}
let delay = retry_mechanism.calculate_delay(attempt);
backoff_sleep(Duration::from_millis(delay)).await;
}
}
}
}
pub fn get_health_status(&self) -> Result<HashMap<String, ServiceHealth>> {
let breakers = lock_safe!(
self.circuit_breakers,
"resilience manager circuit breakers lock for health"
)?;
let mut health_status = HashMap::new();
for (service_name, breaker) in breakers.iter() {
let stats = breaker.stats()?;
let state = breaker.state()?;
let health = ServiceHealth {
service_name: service_name.clone(),
state,
total_calls: stats.total_calls,
successful_calls: stats.successful_calls,
failed_calls: stats.failed_calls,
rejected_calls: stats.rejected_calls,
success_rate: if stats.total_calls > 0 {
(stats.successful_calls as f64 / stats.total_calls as f64) * 100.0
} else {
0.0
},
last_failure_time: stats.last_failure_time,
};
health_status.insert(service_name.clone(), health);
}
Ok(health_status)
}
}
#[derive(Debug, Clone)]
pub struct ServiceHealth {
pub service_name: String,
pub state: CircuitState,
pub total_calls: u64,
pub successful_calls: u64,
pub failed_calls: u64,
pub rejected_calls: u64,
pub success_rate: f64,
pub last_failure_time: Option<Instant>,
}
impl Default for ResilienceManager {
fn default() -> Self {
Self::new()
}
}
#[allow(async_fn_in_trait)]
pub trait ResilientOperation<T> {
async fn execute_resilient(self, manager: &ResilienceManager, service_name: &str) -> Result<T>;
}
impl<F, T, E> ResilientOperation<T> for F
where
F: Fn() -> std::result::Result<T, E> + Send + Sync,
E: std::fmt::Display + std::fmt::Debug + Send + Sync,
{
async fn execute_resilient(self, manager: &ResilienceManager, service_name: &str) -> Result<T> {
manager.execute_with_resilience(service_name, self).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
#[test]
fn test_circuit_breaker_basic_operations() {
let config = CircuitBreakerConfig {
failure_threshold: 3,
minimum_calls: 5,
..Default::default()
};
let cb = CircuitBreaker::new(config);
assert_eq!(
cb.state().expect("operation should succeed"),
CircuitState::Closed
);
assert!(cb.can_execute().expect("operation should succeed"));
for _ in 0..3 {
cb.record_success().expect("operation should succeed");
}
assert_eq!(
cb.state().expect("operation should succeed"),
CircuitState::Closed
);
for _ in 0..2 {
cb.record_failure().expect("operation should succeed");
}
assert_eq!(
cb.state().expect("operation should succeed"),
CircuitState::Closed
);
cb.record_failure().expect("operation should succeed");
assert_eq!(
cb.state().expect("operation should succeed"),
CircuitState::Open
);
assert!(!cb.can_execute().expect("operation should succeed"));
}
#[tokio::test]
async fn test_retry_mechanism() {
let config = RetryConfig {
max_attempts: 5,
base_delay_ms: 1,
backoff_strategy: BackoffStrategy::Fixed,
jitter: false,
retryable_errors: vec!["transient".to_string()],
..Default::default()
};
let retry = RetryMechanism::new(config);
let attempts = Arc::new(AtomicU32::new(0));
let attempts_clone = attempts.clone();
let result = retry
.execute(move || {
let n = attempts_clone.fetch_add(1, Ordering::SeqCst) + 1;
if n < 3 {
Err(format!("transient failure #{n}"))
} else {
Ok(42)
}
})
.await
.expect("operation should eventually succeed after transient failures");
assert_eq!(result, 42);
assert_eq!(
attempts.load(Ordering::SeqCst),
3,
"expected exactly 2 failed attempts followed by 1 successful attempt"
);
}
#[tokio::test]
async fn test_retry_mechanism_stops_on_non_retryable_error() {
let config = RetryConfig {
max_attempts: 5,
base_delay_ms: 1,
jitter: false,
retryable_errors: vec!["transient".to_string()],
..Default::default()
};
let retry = RetryMechanism::new(config);
let attempts = Arc::new(AtomicU32::new(0));
let attempts_clone = attempts.clone();
let result: Result<()> = retry
.execute(move || {
attempts_clone.fetch_add(1, Ordering::SeqCst);
Err::<(), _>("permanent failure".to_string())
})
.await;
assert!(result.is_err());
assert_eq!(
attempts.load(Ordering::SeqCst),
1,
"a non-retryable error must not be retried"
);
}
#[test]
fn test_is_retryable_structural_classification() {
assert!(is_retryable(&Error::ConnectionError("refused".into())));
assert!(is_retryable(&Error::TimeoutError("deadline".into())));
assert!(is_retryable(&Error::IoError("disk busy".into())));
assert!(is_retryable(&Error::ExecutorError("worker crashed".into())));
assert!(!is_retryable(&Error::InvalidInput("bad request".into())));
assert!(!is_retryable(&Error::ColumnNotFound("missing".into())));
}
#[test]
fn test_resilience_manager() {
let manager = ResilienceManager::new();
let _cb1 = manager.get_circuit_breaker("test_service");
let _cb2 = manager.get_circuit_breaker("test_service");
let config = RetryConfig {
max_attempts: 5,
..Default::default()
};
manager
.set_retry_config("test_service", config.clone())
.expect("operation should succeed");
let retrieved_config = manager
.get_retry_config("test_service")
.expect("operation should succeed");
assert_eq!(retrieved_config.max_attempts, 5);
}
}