use std::fmt;
use std::future::Future;
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Closed,
Open,
HalfOpen,
}
struct Inner {
state: State,
failure_count: u32,
opened_at: Option<Instant>,
}
#[derive(Debug)]
pub enum CircuitBreakerError<E> {
Open,
Inner(E),
}
impl<E: fmt::Display> fmt::Display for CircuitBreakerError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Open => write!(f, "circuit breaker is open"),
Self::Inner(err) => write!(f, "{err}"),
}
}
}
impl<E: fmt::Debug + fmt::Display> std::error::Error for CircuitBreakerError<E> {}
pub struct CircuitBreaker {
failure_threshold: u32,
reset_timeout: Duration,
inner: Mutex<Inner>,
}
impl CircuitBreaker {
pub fn new(failure_threshold: u32, reset_timeout: Duration) -> Self {
Self {
failure_threshold,
reset_timeout,
inner: Mutex::new(Inner {
state: State::Closed,
failure_count: 0,
opened_at: None,
}),
}
}
pub async fn execute<F, Fut, T, E>(&self, f: F) -> Result<T, CircuitBreakerError<E>>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
{
let mut inner = self.inner.lock().unwrap();
if inner.state == State::Open {
let elapsed = inner.opened_at.map(|at| at.elapsed()).unwrap_or_default();
if elapsed < self.reset_timeout {
return Err(CircuitBreakerError::Open);
}
inner.state = State::HalfOpen;
}
}
match f().await {
Ok(value) => {
self.on_success();
Ok(value)
}
Err(err) => {
self.on_failure();
Err(CircuitBreakerError::Inner(err))
}
}
}
fn on_success(&self) {
let mut inner = self.inner.lock().unwrap();
inner.failure_count = 0;
inner.state = State::Closed;
}
fn on_failure(&self) {
let mut inner = self.inner.lock().unwrap();
inner.failure_count += 1;
if inner.state == State::HalfOpen || inner.failure_count >= self.failure_threshold {
inner.state = State::Open;
inner.opened_at = Some(Instant::now());
}
}
}
impl Default for CircuitBreaker {
fn default() -> Self {
Self::new(5, Duration::from_secs(30))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn opens_after_the_failure_threshold_is_reached() {
let breaker = CircuitBreaker::new(2, Duration::from_secs(30));
for _ in 0..2 {
let _ = breaker
.execute(|| async { Err::<(), _>(anyhow::anyhow!("boom")) })
.await;
}
let result = breaker
.execute(|| async { Ok::<_, anyhow::Error>(()) })
.await;
assert!(matches!(result, Err(CircuitBreakerError::Open)));
}
#[tokio::test]
async fn a_success_resets_the_failure_count() {
let breaker = CircuitBreaker::new(2, Duration::from_secs(30));
let _ = breaker
.execute(|| async { Err::<(), _>(anyhow::anyhow!("boom")) })
.await;
breaker
.execute(|| async { Ok::<_, anyhow::Error>(()) })
.await
.unwrap();
let result = breaker
.execute(|| async { Err::<(), _>(anyhow::anyhow!("boom")) })
.await;
assert!(matches!(result, Err(CircuitBreakerError::Inner(_))));
let ok = breaker
.execute(|| async { Ok::<_, anyhow::Error>(()) })
.await;
assert!(ok.is_ok());
}
}