use crate::core::{ArcScanner, ScanError};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct CircuitBreakerConfig {
pub failure_threshold: u32,
pub success_threshold: u32,
pub open_duration: Duration,
pub half_open_max_probes: u32,
pub failure_policy: FailurePolicy,
pub fallback_behavior: FallbackBehavior,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
success_threshold: 3,
open_duration: Duration::from_secs(30),
half_open_max_probes: 1,
failure_policy: FailurePolicy::default(),
fallback_behavior: FallbackBehavior::FailClosed,
}
}
}
impl CircuitBreakerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_failure_threshold(mut self, threshold: u32) -> Self {
self.failure_threshold = threshold;
self
}
pub fn with_success_threshold(mut self, threshold: u32) -> Self {
self.success_threshold = threshold;
self
}
pub fn with_open_duration(mut self, duration: Duration) -> Self {
self.open_duration = duration;
self
}
pub fn with_half_open_max_probes(mut self, max: u32) -> Self {
self.half_open_max_probes = max;
self
}
pub fn with_failure_policy(mut self, policy: FailurePolicy) -> Self {
self.failure_policy = policy;
self
}
pub fn with_fallback_behavior(mut self, behavior: FallbackBehavior) -> Self {
self.fallback_behavior = behavior;
self
}
pub fn strict() -> Self {
Self {
failure_threshold: 3,
success_threshold: 5,
open_duration: Duration::from_secs(60),
half_open_max_probes: 1,
failure_policy: FailurePolicy::default(),
fallback_behavior: FallbackBehavior::FailClosed,
}
}
pub fn high_availability() -> Self {
Self {
failure_threshold: 10,
success_threshold: 2,
open_duration: Duration::from_secs(10),
half_open_max_probes: 3,
failure_policy: FailurePolicy::default(),
fallback_behavior: FallbackBehavior::FailClosed,
}
}
}
#[derive(Debug, Clone)]
pub struct FailurePolicy {
pub count_timeouts: bool,
pub count_connection_failures: bool,
pub count_engine_unavailable: bool,
pub count_rate_limited: bool,
pub count_all_errors: bool,
}
impl Default for FailurePolicy {
fn default() -> Self {
Self {
count_timeouts: true,
count_connection_failures: true,
count_engine_unavailable: true,
count_rate_limited: false, count_all_errors: false,
}
}
}
impl FailurePolicy {
pub fn new() -> Self {
Self::default()
}
pub fn all_errors() -> Self {
Self {
count_all_errors: true,
..Self::default()
}
}
pub fn connection_only() -> Self {
Self {
count_timeouts: true,
count_connection_failures: true,
count_engine_unavailable: true,
count_rate_limited: false,
count_all_errors: false,
}
}
pub fn should_count(&self, error: &ScanError) -> bool {
if self.count_all_errors {
return true;
}
match error {
ScanError::Timeout { .. } => self.count_timeouts,
ScanError::ConnectionFailed { .. } => self.count_connection_failures,
ScanError::EngineUnavailable { .. } => self.count_engine_unavailable,
ScanError::RateLimited { .. } => self.count_rate_limited,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub enum FallbackBehavior {
FailClosed,
FailOpen,
Fallback(ArcScanner),
}
impl FallbackBehavior {
pub fn blocks_on_open(&self) -> bool {
matches!(self, Self::FailClosed)
}
pub fn allows_on_open(&self) -> bool {
matches!(self, Self::FailOpen)
}
pub fn has_fallback(&self) -> bool {
matches!(self, Self::Fallback(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = CircuitBreakerConfig::default();
assert_eq!(config.failure_threshold, 5);
assert_eq!(config.success_threshold, 3);
assert_eq!(config.open_duration, Duration::from_secs(30));
}
#[test]
fn test_config_builder() {
let config = CircuitBreakerConfig::new()
.with_failure_threshold(10)
.with_open_duration(Duration::from_secs(60));
assert_eq!(config.failure_threshold, 10);
assert_eq!(config.open_duration, Duration::from_secs(60));
}
#[test]
fn test_failure_policy() {
let policy = FailurePolicy::default();
let timeout_err = ScanError::timeout("test", Duration::from_secs(30));
assert!(policy.should_count(&timeout_err));
let rate_limit_err = ScanError::RateLimited {
engine: "test".into(),
retry_after: None,
};
assert!(!policy.should_count(&rate_limit_err));
}
#[test]
fn test_fallback_behavior() {
assert!(FallbackBehavior::FailClosed.blocks_on_open());
assert!(FallbackBehavior::FailOpen.allows_on_open());
}
}