circuitbreaker_rs/
policy.rs

1//! Policy engine for circuit breaker trip and reset decisions.
2
3use crate::metrics::{BreakerStats, EMAWindow, FixedWindow};
4use std::time::Duration;
5
6/// A policy that determines when to trip and reset a circuit breaker.
7pub trait BreakerPolicy: Send + Sync + 'static {
8    /// Determines if the circuit should trip open based on current stats.
9    fn should_trip(&self, stats: &BreakerStats) -> bool;
10
11    /// Determines if the circuit should reset to closed based on current stats.
12    fn should_reset(&self, stats: &BreakerStats) -> bool;
13}
14
15/// Default policy implementation based on error rate and consecutive failures.
16pub struct DefaultPolicy {
17    failure_threshold: f64,
18    min_throughput: u64,
19    consecutive_failures_threshold: u64,
20    consecutive_successes_threshold: u64,
21}
22
23impl DefaultPolicy {
24    /// Creates a new default policy.
25    pub fn new(
26        failure_threshold: f64,
27        min_throughput: u64,
28        consecutive_failures_threshold: u64,
29        consecutive_successes_threshold: u64,
30    ) -> Self {
31        Self {
32            failure_threshold,
33            min_throughput,
34            consecutive_failures_threshold,
35            consecutive_successes_threshold,
36        }
37    }
38}
39
40impl BreakerPolicy for DefaultPolicy {
41    fn should_trip(&self, stats: &BreakerStats) -> bool {
42        // Trip if error rate exceeds threshold and we have minimum throughput
43        let error_rate = stats.error_rate();
44        let total_calls = stats.get_total_calls();
45
46        if total_calls >= self.min_throughput && error_rate >= self.failure_threshold {
47            return true;
48        }
49
50        // Or if consecutive failures exceed threshold
51        stats.consecutive_failures() >= self.consecutive_failures_threshold
52    }
53
54    fn should_reset(&self, stats: &BreakerStats) -> bool {
55        stats.consecutive_successes() >= self.consecutive_successes_threshold
56    }
57}
58
59/// Time-based policy that considers time windows for decisions.
60pub struct TimeBasedPolicy {
61    window: FixedWindow,
62    failure_threshold: f64,
63    min_call_count: u64,
64    min_recovery_time: Duration,
65    consecutive_successes_threshold: u64,
66}
67
68impl TimeBasedPolicy {
69    /// Creates a new time-based policy.
70    pub fn new(
71        window_size: Duration,
72        bucket_count: usize,
73        failure_threshold: f64,
74        min_call_count: u64,
75        min_recovery_time: Duration,
76        consecutive_successes_threshold: u64,
77    ) -> Self {
78        Self {
79            window: FixedWindow::new(window_size, bucket_count),
80            failure_threshold,
81            min_call_count,
82            min_recovery_time,
83            consecutive_successes_threshold,
84        }
85    }
86
87    /// Records a successful call in the time window.
88    pub fn record_success(&self) {
89        self.window.record_success();
90    }
91
92    /// Records a failed call in the time window.
93    pub fn record_failure(&self) {
94        self.window.record_failure();
95    }
96}
97
98impl BreakerPolicy for TimeBasedPolicy {
99    fn should_trip(&self, stats: &BreakerStats) -> bool {
100        let window_error_rate = self.window.error_rate();
101        let total_calls = stats.get_total_calls();
102
103        window_error_rate >= self.failure_threshold && total_calls >= self.min_call_count
104    }
105
106    fn should_reset(&self, stats: &BreakerStats) -> bool {
107        let last_failure = stats.get_last_failure_time();
108
109        if let Some(time) = last_failure {
110            if time.elapsed() < self.min_recovery_time {
111                return false;
112            }
113        }
114
115        stats.consecutive_successes() >= self.consecutive_successes_threshold
116    }
117}
118
119/// Throughput-aware policy that uses EMA for error rate tracking.
120pub struct ThroughputAwarePolicy {
121    ema_window: EMAWindow,
122    failure_threshold: f64,
123    min_throughput_per_second: f64,
124    throughput_window: Duration,
125    recovery_threshold: f64,
126}
127
128impl ThroughputAwarePolicy {
129    /// Creates a new throughput-aware policy.
130    pub fn new(
131        alpha: f64,
132        calls_required: u64,
133        failure_threshold: f64,
134        min_throughput_per_second: f64,
135        throughput_window: Duration,
136        recovery_threshold: f64,
137    ) -> Self {
138        Self {
139            ema_window: EMAWindow::new(alpha, calls_required),
140            failure_threshold,
141            min_throughput_per_second,
142            throughput_window,
143            recovery_threshold,
144        }
145    }
146
147    /// Records a successful call in the EMA window.
148    pub fn record_success(&self) {
149        self.ema_window.record_success();
150    }
151
152    /// Records a failed call in the EMA window.
153    pub fn record_failure(&self) {
154        self.ema_window.record_failure();
155    }
156
157    fn calculate_throughput(&self, stats: &BreakerStats) -> f64 {
158        let total_calls = stats.get_total_calls();
159
160        let window_secs = self.throughput_window.as_secs_f64();
161        if window_secs <= 0.0 {
162            return 0.0;
163        }
164
165        total_calls as f64 / window_secs
166    }
167}
168
169impl BreakerPolicy for ThroughputAwarePolicy {
170    fn should_trip(&self, stats: &BreakerStats) -> bool {
171        let error_rate = self.ema_window.error_rate();
172        let throughput = self.calculate_throughput(stats);
173
174        error_rate >= self.failure_threshold && throughput >= self.min_throughput_per_second
175    }
176
177    fn should_reset(&self, _stats: &BreakerStats) -> bool {
178        // Use EMA error rate for recovery decision
179        let error_rate = self.ema_window.error_rate();
180        error_rate <= self.recovery_threshold
181    }
182}