github_mcp/core/
circuit_breaker.rs1use std::fmt;
4use std::future::Future;
5use std::sync::Mutex;
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9enum State {
10 Closed,
11 Open,
12 HalfOpen,
13}
14
15struct Inner {
16 state: State,
17 failure_count: u32,
18 opened_at: Option<Instant>,
19}
20
21#[derive(Debug)]
28pub enum CircuitBreakerError<E> {
29 Open,
30 Inner(E),
31}
32
33impl<E: fmt::Display> fmt::Display for CircuitBreakerError<E> {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::Open => write!(f, "circuit breaker is open"),
37 Self::Inner(err) => write!(f, "{err}"),
38 }
39 }
40}
41
42impl<E: fmt::Debug + fmt::Display> std::error::Error for CircuitBreakerError<E> {}
43
44pub struct CircuitBreaker {
49 failure_threshold: u32,
50 reset_timeout: Duration,
51 inner: Mutex<Inner>,
52}
53
54impl CircuitBreaker {
55 pub fn new(failure_threshold: u32, reset_timeout: Duration) -> Self {
56 Self {
57 failure_threshold,
58 reset_timeout,
59 inner: Mutex::new(Inner {
60 state: State::Closed,
61 failure_count: 0,
62 opened_at: None,
63 }),
64 }
65 }
66
67 pub async fn execute<F, Fut, T, E>(&self, f: F) -> Result<T, CircuitBreakerError<E>>
68 where
69 F: FnOnce() -> Fut,
70 Fut: Future<Output = Result<T, E>>,
71 {
72 {
73 let mut inner = self.inner.lock().unwrap();
74 if inner.state == State::Open {
75 let elapsed = inner.opened_at.map(|at| at.elapsed()).unwrap_or_default();
76 if elapsed < self.reset_timeout {
77 return Err(CircuitBreakerError::Open);
78 }
79 inner.state = State::HalfOpen;
80 }
81 }
82
83 match f().await {
84 Ok(value) => {
85 self.on_success();
86 Ok(value)
87 }
88 Err(err) => {
89 self.on_failure();
90 Err(CircuitBreakerError::Inner(err))
91 }
92 }
93 }
94
95 fn on_success(&self) {
96 let mut inner = self.inner.lock().unwrap();
97 inner.failure_count = 0;
98 inner.state = State::Closed;
99 }
100
101 fn on_failure(&self) {
102 let mut inner = self.inner.lock().unwrap();
103 inner.failure_count += 1;
104 if inner.state == State::HalfOpen || inner.failure_count >= self.failure_threshold {
105 inner.state = State::Open;
106 inner.opened_at = Some(Instant::now());
107 }
108 }
109}
110
111impl Default for CircuitBreaker {
112 fn default() -> Self {
113 Self::new(5, Duration::from_secs(30))
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[tokio::test]
122 async fn opens_after_the_failure_threshold_is_reached() {
123 let breaker = CircuitBreaker::new(2, Duration::from_secs(30));
124
125 for _ in 0..2 {
126 let _ = breaker
127 .execute(|| async { Err::<(), _>(anyhow::anyhow!("boom")) })
128 .await;
129 }
130
131 let result = breaker
132 .execute(|| async { Ok::<_, anyhow::Error>(()) })
133 .await;
134 assert!(matches!(result, Err(CircuitBreakerError::Open)));
135 }
136
137 #[tokio::test]
138 async fn a_success_resets_the_failure_count() {
139 let breaker = CircuitBreaker::new(2, Duration::from_secs(30));
140
141 let _ = breaker
142 .execute(|| async { Err::<(), _>(anyhow::anyhow!("boom")) })
143 .await;
144 breaker
145 .execute(|| async { Ok::<_, anyhow::Error>(()) })
146 .await
147 .unwrap();
148
149 let result = breaker
153 .execute(|| async { Err::<(), _>(anyhow::anyhow!("boom")) })
154 .await;
155 assert!(matches!(result, Err(CircuitBreakerError::Inner(_))));
156
157 let ok = breaker
158 .execute(|| async { Ok::<_, anyhow::Error>(()) })
159 .await;
160 assert!(ok.is_ok());
161 }
162}