Skip to main content

vault_client_rs/
circuit_breaker.rs

1use std::sync::Mutex;
2use std::time::{Duration, Instant};
3
4use crate::types::error::VaultError;
5
6/// Configuration for the client-side circuit breaker
7///
8/// When enabled, the circuit breaker tracks consecutive failures and
9/// short-circuits requests during prolonged outages, reducing latency
10/// and load on an unreachable Vault server
11#[derive(Debug, Clone)]
12pub struct CircuitBreakerConfig {
13    /// Consecutive failures required to trip the circuit (default: 5)
14    pub failure_threshold: u32,
15    /// Time in Open state before allowing a probe request (default: 30s)
16    pub reset_timeout: Duration,
17}
18
19impl Default for CircuitBreakerConfig {
20    fn default() -> Self {
21        Self {
22            failure_threshold: 5,
23            reset_timeout: Duration::from_secs(30),
24        }
25    }
26}
27
28enum State {
29    Closed { consecutive_failures: u32 },
30    Open { since: Instant },
31    HalfOpen,
32}
33
34pub(crate) struct CircuitBreaker {
35    config: CircuitBreakerConfig,
36    state: Mutex<State>,
37}
38
39impl CircuitBreaker {
40    pub fn new(config: CircuitBreakerConfig) -> Self {
41        Self {
42            config,
43            state: Mutex::new(State::Closed {
44                consecutive_failures: 0,
45            }),
46        }
47    }
48
49    /// Returns `Ok(())` if a request may proceed, or
50    /// `Err(VaultError::CircuitOpen)` if the circuit is open
51    pub fn check(&self) -> Result<(), VaultError> {
52        let mut state = self.state.lock().map_err(|_| VaultError::LockPoisoned)?;
53        match *state {
54            State::Closed { .. } => Ok(()),
55            State::Open { since } => {
56                if since.elapsed() >= self.config.reset_timeout {
57                    *state = State::HalfOpen;
58                    Ok(())
59                } else {
60                    Err(VaultError::CircuitOpen)
61                }
62            }
63            State::HalfOpen => {
64                // Only one probe at a time; subsequent requests while
65                // half-open are rejected until the probe completes
66                Err(VaultError::CircuitOpen)
67            }
68        }
69    }
70
71    /// Record a successful response, resetting the circuit to Closed
72    pub fn record_success(&self) {
73        if let Ok(mut state) = self.state.lock() {
74            *state = State::Closed {
75                consecutive_failures: 0,
76            };
77        }
78    }
79
80    /// Record a failure, incrementing the counter and potentially
81    /// transitioning to Open
82    pub fn record_failure(&self) {
83        if let Ok(mut state) = self.state.lock() {
84            match *state {
85                State::Closed {
86                    consecutive_failures,
87                } => {
88                    let new_count = consecutive_failures + 1;
89                    if new_count >= self.config.failure_threshold {
90                        *state = State::Open {
91                            since: Instant::now(),
92                        };
93                    } else {
94                        *state = State::Closed {
95                            consecutive_failures: new_count,
96                        };
97                    }
98                }
99                State::HalfOpen => {
100                    // Probe failed, back to Open
101                    *state = State::Open {
102                        since: Instant::now(),
103                    };
104                }
105                State::Open { .. } => {
106                    // Already open, nothing to do
107                }
108            }
109        }
110    }
111}