use axum::body::HttpBody;
use axum::error_handling::HandleErrorLayer;
use bytes::Bytes;
use dragonfly_client_util::ratelimiter::bbr::BBR;
use std::convert::Infallible;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use tonic::body::Body;
use tonic::codegen::http::{Request, Response};
use tonic::service::{LayerExt, Layered};
use tonic::Status;
use tower::{
buffer::BufferLayer,
limit::rate::RateLimitLayer,
load_shed::{error::Overloaded, LoadShedLayer},
util::option_layer,
BoxError, Layer, Service, ServiceBuilder,
};
pub fn rate_limit<S, B>(
service: S,
request_rate_limit: u64,
request_buffer_size: usize,
bbr: Option<Arc<BBR>>,
) -> Layered<
impl Service<
Request<Body>,
Response = axum::response::Response,
Error = Infallible,
Future: Send + 'static,
> + Clone
+ Send
+ Sync
+ 'static,
S,
>
where
S: Service<Request<Body>, Response = Response<B>, Error = Infallible>
+ Clone
+ Send
+ Sync
+ 'static,
S::Future: Send + 'static,
B: HttpBody<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,
{
ServiceBuilder::new()
.layer(option_layer(bbr.map(BBRLayer::new)))
.layer(HandleErrorLayer::new(|err: BoxError| async move {
if err.is::<Overloaded>() {
Status::resource_exhausted(
"server is overloaded: too many requests, please retry later",
)
} else {
Status::internal(err.to_string())
}
.into_http::<Body>()
}))
.layer(LoadShedLayer::new())
.layer(BufferLayer::new(request_buffer_size))
.layer(RateLimitLayer::new(
request_rate_limit,
Duration::from_secs(1),
))
.named_layer(service)
}
#[derive(Clone)]
pub struct BBRLayer {
bbr: Arc<BBR>,
}
impl BBRLayer {
pub fn new(bbr: Arc<BBR>) -> Self {
Self { bbr }
}
}
impl<S> Layer<S> for BBRLayer {
type Service = BBRService<S>;
fn layer(&self, inner: S) -> Self::Service {
BBRService {
inner,
bbr: self.bbr.clone(),
}
}
}
#[derive(Clone)]
pub struct BBRService<S> {
inner: S,
bbr: Arc<BBR>,
}
impl<S, B> Service<Request<Body>> for BBRService<S>
where
S: Service<Request<Body>, Response = Response<B>> + Clone + Send + 'static,
B: Default,
S::Future: Send + 'static,
S::Error: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
let bbr = self.bbr.clone();
let inner_clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, inner_clone);
Box::pin(async move {
let _guard = match bbr.acquire().await {
Some(guard) => guard,
None => {
return Ok(Status::resource_exhausted(
"server is overloaded: CPU/memory thresholds are exceeded, please retry later",
)
.into_http());
}
};
inner.call(req).await
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use dragonfly_client_util::ratelimiter::bbr::BBRConfig;
use futures::future::join_all;
use tower::{service_fn, ServiceExt};
#[tokio::test]
async fn rate_limit_sheds_requests_over_the_buffer_with_resource_exhausted() {
let mut service = rate_limit(
service_fn(|_request: Request<Body>| async {
Ok::<_, Infallible>(Response::new(Body::empty()))
}),
1,
1,
None,
);
let mut responses = Vec::new();
for _ in 0..4 {
responses.push(
service
.ready()
.await
.unwrap()
.call(Request::new(Body::empty())),
);
}
let grpc_statuses: Vec<Option<String>> = join_all(responses)
.await
.into_iter()
.map(|response| {
response
.unwrap()
.headers()
.get("grpc-status")
.map(|status| status.to_str().unwrap().to_string())
})
.collect();
assert!(grpc_statuses[0].is_none());
assert!(grpc_statuses.contains(&Some("8".to_string())));
}
#[tokio::test]
async fn bbr_service_forwards_requests_when_not_overloaded() {
let bbr = Arc::new(
BBR::new(BBRConfig {
cpu_threshold: 100,
memory_threshold: 100,
..Default::default()
})
.await,
);
let mut service = BBRLayer::new(bbr).layer(service_fn(|_request: Request<Body>| async {
Ok::<_, Infallible>(Response::new(Body::empty()))
}));
let response = service
.ready()
.await
.unwrap()
.call(Request::new(Body::empty()))
.await
.unwrap();
assert_eq!(response.status(), http::StatusCode::OK);
assert!(response.headers().get("grpc-status").is_none());
}
}