Skip to main content

sova_http/
breaker.rs

1//! Per-host circuit breaker.
2
3use std::collections::HashMap;
4use std::sync::Mutex;
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone)]
8pub struct BreakerConfig {
9    pub failure_threshold: u32,
10    pub open_for: Duration,
11}
12
13impl Default for BreakerConfig {
14    fn default() -> Self {
15        Self {
16            failure_threshold: 5,
17            open_for: Duration::from_secs(30),
18        }
19    }
20}
21
22#[derive(Debug, Default)]
23struct HostState {
24    failures: u32,
25    open_until: Option<Instant>,
26}
27
28#[derive(Debug, Default)]
29pub struct CircuitBreaker {
30    inner: Mutex<HashMap<String, HostState>>,
31    cfg: BreakerConfig,
32}
33
34impl CircuitBreaker {
35    pub fn new(cfg: BreakerConfig) -> Self {
36        Self {
37            inner: Mutex::new(HashMap::new()),
38            cfg,
39        }
40    }
41
42    pub fn guard(&self, host: &str) -> bool {
43        let mut g = self.inner.lock().unwrap();
44        let st = g.entry(host.to_string()).or_default();
45        if let Some(until) = st.open_until {
46            if Instant::now() < until {
47                return false;
48            }
49            st.open_until = None;
50            st.failures = 0;
51        }
52        true
53    }
54
55    pub fn record_success(&self, host: &str) {
56        let mut g = self.inner.lock().unwrap();
57        if let Some(st) = g.get_mut(host) {
58            st.failures = 0;
59            st.open_until = None;
60        }
61    }
62
63    pub fn record_failure(&self, host: &str) {
64        let mut g = self.inner.lock().unwrap();
65        let st = g.entry(host.to_string()).or_default();
66        st.failures = st.failures.saturating_add(1);
67        if st.failures >= self.cfg.failure_threshold {
68            st.open_until = Some(Instant::now() + self.cfg.open_for);
69        }
70    }
71}