#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
)
)]
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
use axum::body::Body;
use axum::http::{HeaderValue, Request, Response, header::RETRY_AFTER};
use axum::response::IntoResponse;
use pin_project_lite::pin_project;
use tower::{Layer, Service};
use crate::middleware::MetricsCollector;
use crate::middleware::maintenance::{health_prefix_matches, prefix_with_trailing_slash};
const RETRY_AFTER_SECS: &str = "1";
#[derive(Clone, Copy, Debug)]
pub struct LoadShedExempt;
#[derive(Clone)]
pub struct LoadShedLayer {
limit: usize,
in_flight: Arc<AtomicUsize>,
metrics: MetricsCollector,
health_prefix: String,
health_prefix_slash: String,
probe_paths: Vec<String>,
cors: Option<Arc<crate::config::CorsConfig>>,
}
impl LoadShedLayer {
#[must_use]
pub fn new(limit: usize, metrics: MetricsCollector) -> Self {
Self {
limit,
in_flight: Arc::new(AtomicUsize::new(0)),
metrics,
health_prefix: String::new(),
health_prefix_slash: String::new(),
probe_paths: Vec::new(),
cors: None,
}
}
#[must_use]
pub fn with_health_prefix(mut self, prefix: impl Into<String>) -> Self {
self.health_prefix = prefix.into();
self.health_prefix_slash = prefix_with_trailing_slash(&self.health_prefix);
self
}
#[must_use]
pub fn with_probe_paths(mut self, paths: Vec<String>) -> Self {
self.probe_paths = paths;
self
}
#[must_use]
pub fn with_cors(mut self, cors: Option<Arc<crate::config::CorsConfig>>) -> Self {
self.cors = cors;
self
}
}
impl<S> Layer<S> for LoadShedLayer {
type Service = LoadShedService<S>;
fn layer(&self, inner: S) -> Self::Service {
LoadShedService {
inner,
layer: self.clone(),
}
}
}
#[derive(Clone)]
pub struct LoadShedService<S> {
inner: S,
layer: LoadShedLayer,
}
impl<S> LoadShedService<S> {
fn is_exempt<B>(&self, req: &Request<B>) -> bool {
let path = req.uri().path();
let prefix_matched = health_prefix_matches(
path,
&self.layer.health_prefix,
&self.layer.health_prefix_slash,
);
prefix_matched
|| self.layer.probe_paths.iter().any(|probe| probe == path)
|| req.extensions().get::<LoadShedExempt>().is_some()
}
}
impl<S, ReqBody> Service<Request<ReqBody>> for LoadShedService<S>
where
S: Service<Request<ReqBody>, Response = Response<Body>>,
{
type Response = Response<Body>;
type Error = S::Error;
type Future = LoadShedFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
if self.layer.limit == 0 || self.is_exempt(&req) {
return LoadShedFuture::Forward {
inner: self.inner.call(req),
guard: None,
};
}
let in_flight = &self.layer.in_flight;
let mut current = in_flight.load(Ordering::Acquire);
loop {
if current >= self.layer.limit {
self.layer.metrics.record_request_shed();
let cors_origin = self
.layer
.cors
.as_ref()
.and_then(|_| req.headers().get(http::header::ORIGIN).cloned());
return LoadShedFuture::ShortCircuit {
response: Some(build_shed_response(
self.layer.cors.as_deref(),
cors_origin.as_ref(),
)),
};
}
match in_flight.compare_exchange_weak(
current,
current + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break,
Err(observed) => current = observed,
}
}
LoadShedFuture::Forward {
inner: self.inner.call(req),
guard: Some(InFlightGuard {
counter: Arc::clone(in_flight),
}),
}
}
}
struct InFlightGuard {
counter: Arc<AtomicUsize>,
}
impl Drop for InFlightGuard {
fn drop(&mut self) {
self.counter.fetch_sub(1, Ordering::AcqRel);
}
}
fn build_shed_response(
cors: Option<&crate::config::CorsConfig>,
origin: Option<&HeaderValue>,
) -> Response<Body> {
let mut response = crate::error::AutumnError::service_unavailable_msg(
"Too many concurrent requests; try again shortly.",
)
.into_response();
response
.headers_mut()
.insert(RETRY_AFTER, HeaderValue::from_static(RETRY_AFTER_SECS));
if let Some(cors) = cors {
crate::router::mirror_cors_headers(cors, origin, &mut response);
}
response
}
pin_project! {
#[project = LoadShedFutureProj]
pub enum LoadShedFuture<F> {
ShortCircuit { response: Option<Response<Body>> },
Forward {
#[pin]
inner: F,
guard: Option<InFlightGuard>,
},
}
}
impl<F, E> Future for LoadShedFuture<F>
where
F: Future<Output = Result<Response<Body>, E>>,
{
type Output = Result<Response<Body>, E>;
#[allow(
clippy::expect_used,
reason = "unreachable: future not polled after Ready"
)]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() {
LoadShedFutureProj::ShortCircuit { response } => Poll::Ready(Ok(response
.take()
.expect("LoadShedFuture polled after completion"))),
LoadShedFutureProj::Forward { inner, guard } => {
let output = std::task::ready!(inner.poll(cx));
guard.take();
Poll::Ready(output)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::Router;
use axum::routing::get;
use std::sync::atomic::AtomicUsize as StdAtomicUsize;
use std::time::Duration;
use tokio::sync::Notify;
use tower::ServiceExt;
fn make_app(layer: LoadShedLayer) -> Router {
Router::new()
.route("/", get(|| async { "ok" }))
.route("/actuator/health", get(|| async { "healthy" }))
.route("/live", get(|| async { "live" }))
.layer(layer)
}
async fn status(app: Router, uri: &str) -> axum::http::StatusCode {
app.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
.await
.unwrap()
.status()
}
#[tokio::test]
async fn below_ceiling_passes_through() {
let layer = LoadShedLayer::new(10, MetricsCollector::new());
let app = make_app(layer);
assert_eq!(status(app, "/").await, axum::http::StatusCode::OK);
}
#[tokio::test]
async fn disabled_zero_limit_never_sheds() {
let layer = LoadShedLayer::new(0, MetricsCollector::new());
let app = make_app(layer);
for _ in 0..5 {
assert_eq!(status(app.clone(), "/").await, axum::http::StatusCode::OK);
}
}
async fn blocking_handler(gate: Arc<Notify>, entered: Arc<StdAtomicUsize>) -> &'static str {
entered.fetch_add(1, Ordering::SeqCst);
gate.notified().await;
"released"
}
fn make_blocking_app(
layer: LoadShedLayer,
gate: Arc<Notify>,
entered: Arc<StdAtomicUsize>,
) -> Router {
Router::new()
.route(
"/block",
get(move || blocking_handler(gate.clone(), entered.clone())),
)
.route("/", get(|| async { "root" }))
.route("/actuator/health", get(|| async { "healthy" }))
.route("/live", get(|| async { "live" }))
.layer(layer)
}
async fn wait_for_entered(entered: &Arc<StdAtomicUsize>, expected: usize) {
tokio::time::timeout(Duration::from_secs(5), async {
while entered.load(Ordering::SeqCst) < expected {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await
.expect("handlers did not reach the expected in-flight count in time");
}
#[tokio::test]
async fn at_ceiling_sheds_with_503_and_retry_after() {
let metrics = MetricsCollector::new();
let layer = LoadShedLayer::new(2, metrics.clone());
let gate = Arc::new(Notify::new());
let entered = Arc::new(StdAtomicUsize::new(0));
let app = make_blocking_app(layer, gate.clone(), entered.clone());
let mut handles = Vec::new();
for _ in 0..2 {
let app = app.clone();
handles.push(tokio::spawn(async move {
app.oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap()
.status()
}));
}
wait_for_entered(&entered, 2).await;
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
assert!(
resp.headers().contains_key(RETRY_AFTER),
"shed response must carry Retry-After"
);
assert_eq!(metrics.snapshot().http.requests_shed_total, 1);
gate.notify_waiters();
for handle in handles {
assert_eq!(handle.await.unwrap(), axum::http::StatusCode::OK);
}
}
#[tokio::test]
async fn released_slots_are_reusable() {
let metrics = MetricsCollector::new();
let layer = LoadShedLayer::new(1, metrics.clone());
let gate = Arc::new(Notify::new());
let entered = Arc::new(StdAtomicUsize::new(0));
let app = make_blocking_app(layer.clone(), gate.clone(), entered.clone());
let held = {
let app = app.clone();
tokio::spawn(async move {
app.oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap()
.status()
})
};
wait_for_entered(&entered, 1).await;
assert_eq!(layer.in_flight.load(Ordering::Acquire), 1);
gate.notify_waiters();
assert_eq!(held.await.unwrap(), axum::http::StatusCode::OK);
tokio::time::timeout(Duration::from_secs(5), async {
while layer.in_flight.load(Ordering::Acquire) != 0 {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await
.expect("in-flight counter should return to 0 after completion");
assert_eq!(
status(app, "/actuator/health").await,
axum::http::StatusCode::OK
);
}
#[tokio::test]
async fn cancelled_request_still_releases_its_slot() {
let metrics = MetricsCollector::new();
let layer = LoadShedLayer::new(1, metrics);
let gate = Arc::new(Notify::new());
let entered = Arc::new(StdAtomicUsize::new(0));
let app = make_blocking_app(layer.clone(), gate.clone(), entered.clone());
let fut = app.clone().oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
);
let mut fut = Box::pin(fut);
let () = futures::future::poll_fn(|cx| {
let _ = Pin::new(&mut fut).poll(cx);
Poll::Ready(())
})
.await;
wait_for_entered(&entered, 1).await;
assert_eq!(layer.in_flight.load(Ordering::Acquire), 1);
drop(fut);
assert_eq!(layer.in_flight.load(Ordering::Acquire), 0);
}
#[tokio::test]
async fn guard_is_released_as_soon_as_inner_future_resolves() {
let metrics = MetricsCollector::new();
let layer = LoadShedLayer::new(1, metrics);
let app = make_app(layer.clone());
let mut fut =
Box::pin(app.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()));
let resp = std::future::poll_fn(|cx| Pin::new(&mut fut).poll(cx)).await;
assert_eq!(resp.unwrap().status(), axum::http::StatusCode::OK);
assert_eq!(
layer.in_flight.load(Ordering::Acquire),
0,
"slot must be released on Poll::Ready, not deferred until drop"
);
}
#[tokio::test]
async fn load_shed_exempt_marker_bypasses_the_ceiling() {
let metrics = MetricsCollector::new();
let layer = LoadShedLayer::new(1, metrics);
let gate = Arc::new(Notify::new());
let entered = Arc::new(StdAtomicUsize::new(0));
let app = make_blocking_app(layer, gate.clone(), entered.clone());
let held = {
let app = app.clone();
tokio::spawn(async move {
app.oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap()
.status()
})
};
wait_for_entered(&entered, 1).await;
assert_eq!(
status(app.clone(), "/block").await,
axum::http::StatusCode::SERVICE_UNAVAILABLE,
"sanity: the ceiling is actually saturated"
);
let mut exempt_req = Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap();
exempt_req.extensions_mut().insert(LoadShedExempt);
let exempt_fut = {
let app = app.clone();
tokio::spawn(async move { app.oneshot(exempt_req).await.unwrap().status() })
};
wait_for_entered(&entered, 2).await;
gate.notify_waiters();
assert_eq!(exempt_fut.await.unwrap(), axum::http::StatusCode::OK);
assert_eq!(held.await.unwrap(), axum::http::StatusCode::OK);
}
#[tokio::test]
async fn empty_health_prefix_exempts_root_path_like_maintenance_does() {
let metrics = MetricsCollector::new();
let layer = LoadShedLayer::new(1, metrics)
.with_health_prefix("")
.with_probe_paths(vec![]);
let gate = Arc::new(Notify::new());
let entered = Arc::new(StdAtomicUsize::new(0));
let app = make_blocking_app(layer, gate.clone(), entered.clone());
let held = {
let app = app.clone();
tokio::spawn(async move {
app.oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap()
.status()
})
};
wait_for_entered(&entered, 1).await;
assert_eq!(status(app.clone(), "/").await, axum::http::StatusCode::OK);
gate.notify_waiters();
assert_eq!(held.await.unwrap(), axum::http::StatusCode::OK);
}
#[tokio::test]
async fn shed_503_carries_cors_headers_when_configured() {
let metrics = MetricsCollector::new();
let cors = crate::config::CorsConfig {
allowed_origins: vec!["http://other.example".to_owned()],
..Default::default()
};
let layer = LoadShedLayer::new(1, metrics).with_cors(Some(Arc::new(cors)));
let gate = Arc::new(Notify::new());
let entered = Arc::new(StdAtomicUsize::new(0));
let app = make_blocking_app(layer, gate.clone(), entered.clone());
let held = {
let app = app.clone();
tokio::spawn(async move {
app.oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap()
.status()
})
};
wait_for_entered(&entered, 1).await;
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/block")
.header(http::header::ORIGIN, "http://other.example")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
resp.headers()
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
.and_then(|v| v.to_str().ok()),
Some("http://other.example"),
"shed 503 must mirror the matching CORS origin"
);
gate.notify_waiters();
assert_eq!(held.await.unwrap(), axum::http::StatusCode::OK);
}
#[tokio::test]
async fn actuator_health_bypasses_ceiling() {
let metrics = MetricsCollector::new();
let layer = LoadShedLayer::new(1, metrics)
.with_health_prefix("/actuator")
.with_probe_paths(vec!["/live".to_owned()]);
let gate = Arc::new(Notify::new());
let entered = Arc::new(StdAtomicUsize::new(0));
let app = make_blocking_app(layer, gate.clone(), entered.clone());
let held = {
let app = app.clone();
tokio::spawn(async move {
app.oneshot(
Request::builder()
.uri("/block")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap()
.status()
})
};
wait_for_entered(&entered, 1).await;
assert_eq!(
status(app.clone(), "/actuator/health").await,
axum::http::StatusCode::OK
);
assert_eq!(
status(app.clone(), "/live").await,
axum::http::StatusCode::OK
);
gate.notify_waiters();
assert_eq!(held.await.unwrap(), axum::http::StatusCode::OK);
}
}