Skip to main content

lean_ctx/core/context_kernel/
bounded.rs

1//! Bounded stores, queues, and circuit breakers for kernel HA.
2
3use std::collections::VecDeque;
4use std::time::{Duration, Instant};
5
6/// A first-in, first-out queue with a fixed upper bound.
7#[derive(Debug, Clone)]
8pub struct BoundedQueue<T> {
9    items: VecDeque<T>,
10    max_size: usize,
11}
12
13impl<T> BoundedQueue<T> {
14    /// Creates an empty queue that retains at most `max_size` items.
15    pub fn new(max_size: usize) -> Self {
16        Self {
17            items: VecDeque::with_capacity(max_size),
18            max_size,
19        }
20    }
21
22    /// Appends an item and returns the oldest item when capacity is exceeded.
23    pub fn push(&mut self, item: T) -> Option<T> {
24        if self.max_size == 0 {
25            return Some(item);
26        }
27
28        let evicted = if self.is_full() {
29            self.items.pop_front()
30        } else {
31            None
32        };
33        self.items.push_back(item);
34        evicted
35    }
36
37    /// Returns the number of retained items.
38    pub fn len(&self) -> usize {
39        self.items.len()
40    }
41
42    /// Returns whether the queue contains no items.
43    pub fn is_empty(&self) -> bool {
44        self.items.is_empty()
45    }
46
47    /// Returns whether the queue has reached its configured capacity.
48    pub fn is_full(&self) -> bool {
49        self.items.len() >= self.max_size
50    }
51
52    /// Iterates over retained items from oldest to newest.
53    pub fn iter(&self) -> impl Iterator<Item = &T> {
54        self.items.iter()
55    }
56
57    /// Removes and returns up to `n` oldest items.
58    pub fn drain_oldest(&mut self, n: usize) -> Vec<T> {
59        let count = n.min(self.items.len());
60        self.items.drain(..count).collect()
61    }
62
63    /// Removes all retained items.
64    pub fn clear(&mut self) {
65        self.items.clear();
66    }
67}
68
69/// Operational state of a circuit breaker.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum CircuitState {
72    /// Requests are allowed normally.
73    Closed,
74    /// Requests are rejected until the cooldown elapses.
75    Open,
76    /// Requests are allowed while provider recovery is evaluated.
77    HalfOpen,
78}
79
80/// Failure isolation state for a fallible dependency.
81#[derive(Debug, Clone)]
82pub struct CircuitBreaker {
83    state: CircuitState,
84    failure_count: u32,
85    success_count: u32,
86    failure_threshold: u32,
87    recovery_threshold: u32,
88    last_state_change: Instant,
89    cooldown: Duration,
90}
91
92impl CircuitBreaker {
93    /// Creates a circuit breaker with caller-defined thresholds and cooldown.
94    pub fn new(failure_threshold: u32, recovery_threshold: u32, cooldown: Duration) -> Self {
95        Self {
96            state: CircuitState::Closed,
97            failure_count: 0,
98            success_count: 0,
99            failure_threshold,
100            recovery_threshold,
101            last_state_change: Instant::now(),
102            cooldown,
103        }
104    }
105
106    /// Creates the standard kernel circuit breaker.
107    pub fn default_breaker() -> Self {
108        Self::new(3, 2, Duration::from_secs(30))
109    }
110
111    /// Returns the current circuit state.
112    pub fn state(&self) -> CircuitState {
113        self.state
114    }
115
116    /// Records a successful dependency call.
117    pub fn record_success(&mut self) {
118        match self.state {
119            CircuitState::Closed => {
120                self.failure_count = 0;
121            }
122            CircuitState::HalfOpen => {
123                self.success_count = self.success_count.saturating_add(1);
124                if self.success_count >= self.recovery_threshold {
125                    self.transition_to(CircuitState::Closed);
126                }
127            }
128            CircuitState::Open => {}
129        }
130    }
131
132    /// Records a failed dependency call.
133    pub fn record_failure(&mut self) {
134        match self.state {
135            CircuitState::Closed => {
136                self.failure_count = self.failure_count.saturating_add(1);
137                if self.failure_count >= self.failure_threshold {
138                    self.transition_to(CircuitState::Open);
139                }
140            }
141            CircuitState::HalfOpen => self.transition_to(CircuitState::Open),
142            CircuitState::Open => {}
143        }
144    }
145
146    /// Returns whether a dependency call should proceed.
147    ///
148    /// **Side effect**: if the breaker is `Open` and the cooldown has
149    /// elapsed, this transitions the state to `HalfOpen`.
150    pub fn should_allow(&mut self) -> bool {
151        match self.state {
152            CircuitState::Closed | CircuitState::HalfOpen => true,
153            CircuitState::Open => {
154                if self.last_state_change.elapsed() >= self.cooldown {
155                    self.transition_to(CircuitState::HalfOpen);
156                    true
157                } else {
158                    false
159                }
160            }
161        }
162    }
163
164    /// Forces the circuit into its initial closed state.
165    pub fn reset(&mut self) {
166        self.transition_to(CircuitState::Closed);
167    }
168
169    fn transition_to(&mut self, state: CircuitState) {
170        self.state = state;
171        self.failure_count = 0;
172        self.success_count = 0;
173        self.last_state_change = Instant::now();
174    }
175}
176
177/// Circuit breaker and lifetime call statistics for one provider.
178#[derive(Debug, Clone)]
179pub struct ProviderCircuit {
180    pub provider_id: String,
181    pub breaker: CircuitBreaker,
182    pub total_calls: u64,
183    pub total_failures: u64,
184}
185
186impl ProviderCircuit {
187    /// Creates provider state using the standard kernel breaker settings.
188    pub fn new(provider_id: String) -> Self {
189        Self {
190            provider_id,
191            breaker: CircuitBreaker::default_breaker(),
192            total_calls: 0,
193            total_failures: 0,
194        }
195    }
196
197    /// Returns whether the provider circuit currently permits a call.
198    pub fn is_available(&mut self) -> bool {
199        self.breaker.should_allow()
200    }
201
202    /// Records a completed provider call and updates circuit state.
203    pub fn record_outcome(&mut self, success: bool) {
204        self.total_calls = self.total_calls.saturating_add(1);
205        if success {
206            self.breaker.record_success();
207        } else {
208            self.total_failures = self.total_failures.saturating_add(1);
209            self.breaker.record_failure();
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::{BoundedQueue, CircuitBreaker, CircuitState, ProviderCircuit};
217    use std::time::Duration;
218
219    #[test]
220    fn bounded_queue_evicts_oldest() {
221        let mut queue = BoundedQueue::new(2);
222
223        assert_eq!(queue.push(1), None);
224        assert_eq!(queue.push(2), None);
225        assert_eq!(queue.push(3), Some(1));
226        assert_eq!(queue.iter().copied().collect::<Vec<i32>>(), vec![2, 3]);
227        assert!(queue.is_full());
228    }
229
230    #[test]
231    fn bounded_queue_drain() {
232        let mut queue = BoundedQueue::new(4);
233        queue.push("first");
234        queue.push("second");
235        queue.push("third");
236
237        assert_eq!(queue.drain_oldest(2), vec!["first", "second"]);
238        assert_eq!(queue.len(), 1);
239        assert_eq!(queue.drain_oldest(5), vec!["third"]);
240        assert!(queue.is_empty());
241    }
242
243    #[test]
244    fn bounded_queue_with_zero_capacity_rejects_items() {
245        let mut queue = BoundedQueue::new(0);
246
247        assert_eq!(queue.push(7), Some(7));
248        assert!(queue.is_empty());
249        assert!(queue.is_full());
250    }
251
252    #[test]
253    fn circuit_breaker_opens_after_threshold() {
254        let mut breaker = CircuitBreaker::new(3, 2, Duration::from_secs(30));
255
256        breaker.record_failure();
257        breaker.record_failure();
258        assert_eq!(breaker.state(), CircuitState::Closed);
259        breaker.record_failure();
260
261        assert_eq!(breaker.state(), CircuitState::Open);
262        assert!(!breaker.should_allow());
263    }
264
265    #[test]
266    fn circuit_breaker_recovers_via_half_open() {
267        let mut breaker = CircuitBreaker::new(1, 1, Duration::ZERO);
268        breaker.record_failure();
269
270        assert!(breaker.should_allow());
271        assert_eq!(breaker.state(), CircuitState::HalfOpen);
272        breaker.record_success();
273
274        assert_eq!(breaker.state(), CircuitState::Closed);
275        assert!(breaker.should_allow());
276    }
277
278    #[test]
279    fn provider_circuit_tracks_stats() {
280        let mut circuit = ProviderCircuit::new("filesystem".to_string());
281
282        circuit.record_outcome(true);
283        circuit.record_outcome(false);
284        circuit.record_outcome(false);
285
286        assert_eq!(circuit.total_calls, 3);
287        assert_eq!(circuit.total_failures, 2);
288        assert_eq!(circuit.breaker.state(), CircuitState::Closed);
289    }
290}