use std::sync::Arc;
use axum::{
extract::{Request, State},
http::{HeaderValue, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use crate::resilience::{BulkheadError, BulkheadManager, CircuitBreakerManager};
#[derive(Clone)]
pub struct ResilienceMiddlewareState {
pub circuit_manager: Arc<CircuitBreakerManager>,
pub bulkhead_manager: Arc<BulkheadManager>,
pub default_service: String,
}
impl ResilienceMiddlewareState {
pub fn new(
circuit_manager: Arc<CircuitBreakerManager>,
bulkhead_manager: Arc<BulkheadManager>,
) -> Self {
Self {
circuit_manager,
bulkhead_manager,
default_service: "http".to_string(),
}
}
}
pub fn resilience_state_from_configs(
circuit_config: Option<crate::config::CircuitBreakerConfig>,
bulkhead_config: Option<crate::config::BulkheadConfig>,
) -> (ResilienceMiddlewareState, crate::resilience_api::ResilienceApiState) {
use prometheus::Registry;
let registry = Arc::new(Registry::new());
let circuit =
Arc::new(CircuitBreakerManager::new(circuit_config.unwrap_or_default(), registry.clone()));
let bulkhead = Arc::new(BulkheadManager::new(bulkhead_config.unwrap_or_default(), registry));
let mw_state = ResilienceMiddlewareState::new(circuit.clone(), bulkhead.clone());
let api_state = crate::resilience_api::ResilienceApiState {
circuit_breaker_manager: circuit,
bulkhead_manager: bulkhead,
};
(mw_state, api_state)
}
pub fn default_resilience_state(
) -> (ResilienceMiddlewareState, crate::resilience_api::ResilienceApiState) {
resilience_state_from_configs(None, None)
}
pub async fn resilience_middleware(
State(state): State<ResilienceMiddlewareState>,
request: Request,
next: Next,
) -> Response {
let endpoint = format!("{} {}", request.method().as_str(), request.uri().path());
let breaker = state.circuit_manager.get_breaker(&endpoint).await;
if !breaker.allow_request().await {
return circuit_open_response(&endpoint);
}
let bulkhead = state.bulkhead_manager.get_bulkhead(&state.default_service).await;
let _guard = match bulkhead.try_acquire().await {
Ok(g) => g,
Err(BulkheadError::Rejected) => {
return bulkhead_response(
&state.default_service,
"bulkhead_rejected",
"Bulkhead is full; request rejected.",
);
}
Err(BulkheadError::Timeout) => {
return bulkhead_response(
&state.default_service,
"bulkhead_timeout",
"Bulkhead queue timeout; request not admitted.",
);
}
};
let response = next.run(request).await;
if response.status().is_server_error() {
breaker.record_failure().await;
} else {
breaker.record_success().await;
}
response
}
fn circuit_open_response(endpoint: &str) -> Response {
let body = Json(json!({
"error": "circuit_open",
"endpoint": endpoint,
"message": "Circuit breaker is open for this endpoint; refusing the request.",
}));
let mut resp = (StatusCode::SERVICE_UNAVAILABLE, body).into_response();
resp.headers_mut().insert("retry-after", HeaderValue::from_static("1"));
resp
}
fn bulkhead_response(service: &str, error: &'static str, message: &'static str) -> Response {
let body = Json(json!({
"error": error,
"service": service,
"message": message,
}));
let mut resp = (StatusCode::SERVICE_UNAVAILABLE, body).into_response();
resp.headers_mut().insert("retry-after", HeaderValue::from_static("1"));
resp
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{BulkheadConfig, CircuitBreakerConfig};
use axum::{body::Body, http::Request, middleware, routing::get, Router};
use prometheus::Registry;
use tower::ServiceExt;
async fn app(state: ResilienceMiddlewareState) -> Router {
Router::new()
.route("/ok", get(|| async { (StatusCode::OK, "ok").into_response() }))
.route(
"/boom",
get(|| async { (StatusCode::INTERNAL_SERVER_ERROR, "boom").into_response() }),
)
.route(
"/client-error",
get(|| async { (StatusCode::BAD_REQUEST, "nope").into_response() }),
)
.layer(middleware::from_fn_with_state(state, resilience_middleware))
}
fn state() -> ResilienceMiddlewareState {
let registry = Arc::new(Registry::new());
let circuit = Arc::new(CircuitBreakerManager::new(
CircuitBreakerConfig {
enabled: true,
failure_threshold: 2,
success_threshold: 1,
timeout_ms: 60_000,
half_open_max_requests: 1,
failure_rate_threshold: 50.0,
min_requests_for_rate: 100, rolling_window_ms: 10_000,
},
registry.clone(),
));
let bulkhead = Arc::new(BulkheadManager::new(
BulkheadConfig {
enabled: false, max_concurrent_requests: 4,
max_queue_size: 0,
queue_timeout_ms: 1000,
},
registry,
));
ResilienceMiddlewareState::new(circuit, bulkhead)
}
async fn call(app: &Router, path: &str) -> StatusCode {
let res = app
.clone()
.oneshot(Request::builder().uri(path).body(Body::empty()).unwrap())
.await
.unwrap();
res.status()
}
#[tokio::test]
async fn success_response_is_passed_through() {
let s = state();
let app = app(s.clone()).await;
assert_eq!(call(&app, "/ok").await, StatusCode::OK);
}
#[tokio::test]
async fn consecutive_5xx_opens_breaker_and_returns_503() {
let s = state();
let app = app(s.clone()).await;
assert_eq!(call(&app, "/boom").await, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(call(&app, "/boom").await, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(call(&app, "/boom").await, StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn client_4xx_does_not_trip_breaker() {
let s = state();
let app = app(s.clone()).await;
for _ in 0..5 {
assert_eq!(call(&app, "/client-error").await, StatusCode::BAD_REQUEST);
}
}
#[tokio::test]
async fn breaker_is_per_endpoint() {
let s = state();
let app = app(s.clone()).await;
assert_eq!(call(&app, "/boom").await, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(call(&app, "/boom").await, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(call(&app, "/boom").await, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(call(&app, "/ok").await, StatusCode::OK);
}
#[tokio::test]
async fn bulkhead_rejected_returns_503() {
let registry = Arc::new(Registry::new());
let bulkhead = Arc::new(BulkheadManager::new(
BulkheadConfig {
enabled: true,
max_concurrent_requests: 0,
max_queue_size: 0,
queue_timeout_ms: 100,
},
registry.clone(),
));
let circuit = Arc::new(CircuitBreakerManager::new(
CircuitBreakerConfig::default(), registry,
));
let s = ResilienceMiddlewareState::new(circuit, bulkhead);
let app = app(s).await;
assert_eq!(call(&app, "/ok").await, StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn from_configs_threshold_drives_breaker_trip() {
let cb = CircuitBreakerConfig {
enabled: true,
failure_threshold: 1,
success_threshold: 1,
timeout_ms: 60_000,
half_open_max_requests: 1,
failure_rate_threshold: 100.0,
min_requests_for_rate: 100,
rolling_window_ms: 10_000,
};
let (mw, _api) = resilience_state_from_configs(Some(cb), None);
let app = app(mw).await;
assert_eq!(call(&app, "/boom").await, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(call(&app, "/boom").await, StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn from_configs_bulkhead_capacity_rejects() {
let bh = BulkheadConfig {
enabled: true,
max_concurrent_requests: 0,
max_queue_size: 0,
queue_timeout_ms: 100,
};
let (mw, _api) = resilience_state_from_configs(None, Some(bh));
let app = app(mw).await;
assert_eq!(call(&app, "/ok").await, StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn from_configs_with_none_matches_default_state() {
let (mw, _api) = resilience_state_from_configs(None, None);
let app = app(mw).await;
assert_eq!(call(&app, "/ok").await, StatusCode::OK);
for _ in 0..5 {
assert_eq!(call(&app, "/boom").await, StatusCode::INTERNAL_SERVER_ERROR);
}
}
#[tokio::test]
async fn disabled_managers_pass_through_unchanged() {
let registry = Arc::new(Registry::new());
let circuit =
Arc::new(CircuitBreakerManager::new(CircuitBreakerConfig::default(), registry.clone()));
let bulkhead = Arc::new(BulkheadManager::new(BulkheadConfig::default(), registry));
let s = ResilienceMiddlewareState::new(circuit, bulkhead);
let app = app(s).await;
assert_eq!(call(&app, "/ok").await, StatusCode::OK);
for _ in 0..10 {
assert_eq!(call(&app, "/boom").await, StatusCode::INTERNAL_SERVER_ERROR);
}
}
}