use std::time::Duration;
#[derive(Clone, Debug, PartialEq)]
pub enum ResiliencePolicy {
None,
Retry {
max_attempts: u32,
backoff: BackoffStrategy,
},
CircuitBreaker {
failure_threshold: u32,
recovery_timeout: Duration,
success_threshold: u32,
},
RateLimit {
requests_per_second: u32,
burst_capacity: u32,
},
Timeout { duration: Duration },
Combined { policies: Vec<ResiliencePolicy> },
}
#[derive(Clone, Debug, PartialEq)]
pub enum BackoffStrategy {
Fixed { delay: Duration },
Exponential {
initial_delay: Duration,
multiplier: f64,
max_delay: Option<Duration>,
jitter: bool,
},
Linear {
initial_delay: Duration,
increment: Duration,
max_delay: Option<Duration>,
},
}
impl Default for BackoffStrategy {
fn default() -> Self {
Self::Exponential {
initial_delay: Duration::from_millis(100),
multiplier: 2.0,
max_delay: Some(Duration::from_secs(30)),
jitter: true,
}
}
}
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
pub enum ResilienceDomainError {
#[error("Operation timed out after {duration:?}")]
Timeout { duration: Duration },
#[error("Operation failed after {attempts} attempts")]
RetryExhausted { attempts: u32, last_error: String },
#[error("Circuit breaker is open - service unavailable")]
CircuitOpen,
#[error("Rate limit exceeded - too many requests")]
RateLimited { retry_after: Option<Duration> },
#[error("Operation cancelled")]
Cancelled,
#[error("Infrastructure error: {message}")]
Infrastructure { message: String },
}
impl ResilienceDomainError {
pub fn is_retryable(&self) -> bool {
match self {
Self::Timeout { .. } => true,
Self::RetryExhausted { .. } => false, Self::CircuitOpen => false, Self::RateLimited { .. } => true, Self::Cancelled => false, Self::Infrastructure { .. } => true, }
}
pub fn is_service_unavailable(&self) -> bool {
matches!(self, Self::CircuitOpen)
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
Self::RateLimited { retry_after } => *retry_after,
_ => None,
}
}
}
#[async_trait::async_trait]
pub trait ResilientOperation<T, E> {
fn resilience_policy(&self) -> ResiliencePolicy;
async fn execute(&self) -> Result<T, E>;
fn operation_id(&self) -> &str {
std::any::type_name::<Self>()
}
fn is_critical(&self) -> bool {
true
}
}
#[async_trait::async_trait]
pub trait ResilientService {
fn default_resilience_policy(&self) -> ResiliencePolicy {
ResiliencePolicy::None
}
fn operation_policies(&self) -> std::collections::HashMap<String, ResiliencePolicy> {
std::collections::HashMap::new()
}
fn service_id(&self) -> &str {
std::any::type_name::<Self>()
}
}
#[derive(Clone, Debug)]
pub struct ResilienceConfig {
pub enabled: bool,
pub default_policies: std::collections::HashMap<String, ResiliencePolicy>,
pub service_overrides: std::collections::HashMap<String, ResiliencePolicy>,
}
impl Default for ResilienceConfig {
fn default() -> Self {
Self {
enabled: true,
default_policies: std::collections::HashMap::new(),
service_overrides: std::collections::HashMap::new(),
}
}
}
pub mod policies {
use std::time::Duration;
use super::*;
pub fn retry(max_attempts: u32) -> ResiliencePolicy {
ResiliencePolicy::Retry {
max_attempts,
backoff: BackoffStrategy::default(),
}
}
pub fn circuit_breaker(failure_threshold: u32, recovery_timeout_secs: u64) -> ResiliencePolicy {
ResiliencePolicy::CircuitBreaker {
failure_threshold,
recovery_timeout: Duration::from_secs(recovery_timeout_secs),
success_threshold: 3,
}
}
pub fn rate_limit(requests_per_second: u32) -> ResiliencePolicy {
ResiliencePolicy::RateLimit {
requests_per_second,
burst_capacity: requests_per_second / 4, }
}
pub fn timeout(seconds: u64) -> ResiliencePolicy {
ResiliencePolicy::Timeout {
duration: Duration::from_secs(seconds),
}
}
pub fn combine(policies: Vec<ResiliencePolicy>) -> ResiliencePolicy {
ResiliencePolicy::Combined { policies }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resilience_policy_creation() {
let retry_policy = policies::retry(3);
assert_eq!(
retry_policy,
ResiliencePolicy::Retry {
max_attempts: 3,
backoff: BackoffStrategy::default(),
}
);
let circuit_policy = policies::circuit_breaker(5, 30);
match circuit_policy {
ResiliencePolicy::CircuitBreaker {
failure_threshold,
recovery_timeout,
..
} => {
assert_eq!(failure_threshold, 5);
assert_eq!(recovery_timeout, Duration::from_secs(30));
}
_ => panic!("Expected CircuitBreaker policy"),
}
}
#[test]
fn test_domain_error_retryable() {
assert!(ResilienceDomainError::Timeout {
duration: Duration::from_secs(1)
}
.is_retryable());
assert!(ResilienceDomainError::RateLimited { retry_after: None }.is_retryable());
assert!(!ResilienceDomainError::CircuitOpen.is_retryable());
assert!(!ResilienceDomainError::Cancelled.is_retryable());
}
#[test]
fn test_backoff_strategy_default() {
let strategy = BackoffStrategy::default();
match strategy {
BackoffStrategy::Exponential {
initial_delay,
multiplier,
max_delay,
jitter,
} => {
assert_eq!(initial_delay, Duration::from_millis(100));
assert_eq!(multiplier, 2.0);
assert_eq!(max_delay, Some(Duration::from_secs(30)));
assert!(jitter);
}
_ => panic!("Expected Exponential backoff"),
}
}
#[test]
fn test_combined_policies() {
let retry = policies::retry(3);
let timeout = policies::timeout(10);
let combined = policies::combine(vec![retry.clone(), timeout]);
match combined {
ResiliencePolicy::Combined { policies } => {
assert_eq!(policies.len(), 2);
assert_eq!(policies[0], retry);
}
_ => panic!("Expected Combined policy"),
}
}
}