use circuit_breaker::{CircuitBreaker as ExternalCircuitBreaker, CircuitState as ExternalState};
use futures::future::BoxFuture;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tracing::{debug, error, warn};
pub struct MeilisearchCircuitBreaker {
inner: Arc<Mutex<ExternalCircuitBreaker>>,
name: String,
}
impl MeilisearchCircuitBreaker {
pub fn new(name: String) -> Self {
let circuit_breaker = ExternalCircuitBreaker::new(
5, Duration::from_secs(60), );
Self {
inner: Arc::new(Mutex::new(circuit_breaker)),
name,
}
}
pub fn with_config(name: String, failure_threshold: u32, reset_timeout: Duration) -> Self {
let circuit_breaker = ExternalCircuitBreaker::new(failure_threshold, reset_timeout);
Self {
inner: Arc::new(Mutex::new(circuit_breaker)),
name,
}
}
pub async fn call<F, T, E>(&self, op: F) -> Result<T, CircuitBreakerError<E>>
where
F: FnOnce() -> BoxFuture<'static, Result<T, E>>,
E: std::fmt::Debug,
{
let cb = self.inner.lock().await;
let state = cb.state();
debug!("Circuit breaker '{}' state: {:?}", self.name, state);
match state {
ExternalState::Open => {
error!("Circuit breaker '{}' is open, rejecting request", self.name);
crate::metrics::CIRCUIT_BREAKER_CALLS
.with_label_values(&[&self.name, "rejected"])
.inc();
Err(CircuitBreakerError::CircuitOpen)
}
_ => {
drop(cb);
debug!("Circuit breaker '{}' allowing request", self.name);
let result = op().await;
let cb = self.inner.lock().await;
match &result {
Ok(_) => {
cb.handle_success();
debug!("Circuit breaker '{}' recorded success", self.name);
crate::metrics::CIRCUIT_BREAKER_CALLS
.with_label_values(&[&self.name, "success"])
.inc();
}
Err(e) => {
cb.handle_failure();
warn!("Circuit breaker '{}' recorded error: {:?}", self.name, e);
crate::metrics::CIRCUIT_BREAKER_CALLS
.with_label_values(&[&self.name, "failure"])
.inc();
}
}
result.map_err(CircuitBreakerError::OperationError)
}
}
}
pub async fn get_state(&self) -> CircuitState {
let cb = self.inner.lock().await;
match cb.state() {
ExternalState::Closed => CircuitState::Closed,
ExternalState::Open => CircuitState::Open,
ExternalState::HalfOpen => CircuitState::HalfOpen,
}
}
pub async fn get_stats(&self) -> CircuitBreakerStats {
let cb = self.inner.lock().await;
let state = cb.state();
CircuitBreakerStats {
state: match state {
ExternalState::Closed => CircuitState::Closed,
ExternalState::Open => CircuitState::Open,
ExternalState::HalfOpen => CircuitState::HalfOpen,
},
name: self.name.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Debug, thiserror::Error)]
pub enum CircuitBreakerError<E> {
#[error("Circuit breaker is open")]
CircuitOpen,
#[error("Operation failed: {0:?}")]
OperationError(E),
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerStats {
pub state: CircuitState,
pub name: String,
}
pub struct CircuitBreakerBuilder {
name: String,
failure_threshold: u32,
reset_timeout: Duration,
}
impl CircuitBreakerBuilder {
pub fn new(name: String) -> Self {
Self {
name,
failure_threshold: 5,
reset_timeout: Duration::from_secs(60),
}
}
pub fn error_rate(self, _rate: f64) -> Self {
warn!("Error rate configuration not supported in circuit_breaker 0.1.1, using consecutive failures instead");
self
}
pub fn min_request_count(self, _count: u64) -> Self {
warn!("Min request count configuration not supported in circuit_breaker 0.1.1");
self
}
pub fn consecutive_failures(mut self, count: u64) -> Self {
self.failure_threshold = count as u32;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.reset_timeout = timeout;
self
}
pub fn build(self) -> MeilisearchCircuitBreaker {
MeilisearchCircuitBreaker::with_config(
self.name,
self.failure_threshold,
self.reset_timeout,
)
}
}