servers/grpc/
memory_limit.rs1use std::convert::Infallible;
35use std::sync::Arc;
36use std::task::{Context, Poll};
37
38use axum::response::IntoResponse;
39use common_memory_manager::MemoryGuard;
40use futures::future::BoxFuture;
41use http::Request;
42use tonic::Status;
43use tonic::codegen::Service;
44use tonic::server::NamedService;
45use tower::Layer;
46
47use crate::request_memory_limiter::ServerMemoryLimiter;
48use crate::request_memory_metrics::RequestMemoryMetrics;
49
50const GRPC_ENCODING_HEADER: &str = "grpc-encoding";
52const IDENTITY_ENCODING: &str = "identity";
54
55#[derive(Clone)]
62pub(crate) struct PreDecodeMemoryReservation {
63 _guard: Arc<MemoryGuard<RequestMemoryMetrics>>,
64}
65
66impl PreDecodeMemoryReservation {
67 pub(crate) fn reserved_bytes(&self) -> u64 {
68 self._guard.granted_bytes()
69 }
70}
71
72#[derive(Clone)]
73pub struct MemoryLimiterExtensionLayer {
74 limiter: ServerMemoryLimiter,
75 max_decoding_message_size: usize,
76}
77
78impl MemoryLimiterExtensionLayer {
79 pub fn new(limiter: ServerMemoryLimiter, max_decoding_message_size: usize) -> Self {
80 Self {
81 limiter,
82 max_decoding_message_size,
83 }
84 }
85}
86
87impl<S> Layer<S> for MemoryLimiterExtensionLayer {
88 type Service = MemoryLimiterExtensionService<S>;
89
90 fn layer(&self, service: S) -> Self::Service {
91 MemoryLimiterExtensionService {
92 inner: service,
93 limiter: self.limiter.clone(),
94 max_decoding_message_size: self.max_decoding_message_size,
95 }
96 }
97}
98
99#[derive(Clone)]
100pub struct MemoryLimiterExtensionService<S> {
101 inner: S,
102 limiter: ServerMemoryLimiter,
103 max_decoding_message_size: usize,
104}
105
106impl<S: NamedService> NamedService for MemoryLimiterExtensionService<S> {
107 const NAME: &'static str = S::NAME;
108}
109
110impl<S, ReqBody> Service<Request<ReqBody>> for MemoryLimiterExtensionService<S>
111where
112 S: Service<Request<ReqBody>, Error = Infallible> + Clone + Send + 'static,
113 S::Response: axum::response::IntoResponse,
114 S::Future: Send + 'static,
115 ReqBody: Send + 'static,
116{
117 type Response = axum::response::Response;
118 type Error = Infallible;
119 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
120
121 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
122 self.inner.poll_ready(cx)
123 }
124
125 fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
126 let mut this = self.clone();
129 Box::pin(async move {
130 req.extensions_mut().insert(this.limiter.clone());
131
132 let compressed = req
133 .headers()
134 .get(GRPC_ENCODING_HEADER)
135 .and_then(|value| value.to_str().ok())
136 .is_some_and(|value| !value.eq_ignore_ascii_case(IDENTITY_ENCODING));
137
138 if compressed {
139 let reservation = this.max_decoding_message_size as u64;
144 match this.limiter.acquire(reservation).await {
145 Ok(guard) => {
146 req.extensions_mut().insert(PreDecodeMemoryReservation {
147 _guard: Arc::new(guard),
148 });
149 }
150 Err(e) => {
151 return Ok(Status::resource_exhausted(format!(
152 "request memory limit exceeded: {e}"
153 ))
154 .into_http::<axum::body::Body>()
155 .into_response());
156 }
157 }
158 }
159
160 match this.inner.call(req).await {
161 Ok(response) => Ok(response.into_response()),
162 Err(e) => match e {},
163 }
164 })
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use std::convert::Infallible;
171
172 use axum::body::Body;
173 use axum::response::IntoResponse;
174 use common_memory_manager::OnExhaustedPolicy;
175 use futures_util::future::BoxFuture;
176 use http::{HeaderValue, StatusCode};
177 use tower::ServiceExt;
178
179 use super::*;
180 use crate::grpc::memory_limit::MemoryLimiterExtensionLayer;
181
182 #[derive(Clone)]
183 struct EchoService;
184
185 impl Service<Request<Body>> for EchoService {
186 type Response = axum::response::Response;
187 type Error = Infallible;
188 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
189
190 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
191 Poll::Ready(Ok(()))
192 }
193
194 fn call(&mut self, req: Request<Body>) -> Self::Future {
195 let saw_reservation = req
196 .extensions()
197 .get::<PreDecodeMemoryReservation>()
198 .is_some();
199 let saw_limiter = req.extensions().get::<ServerMemoryLimiter>().is_some();
200 Box::pin(async move {
201 Ok((
202 StatusCode::OK,
203 format!("reservation={saw_reservation} limiter={saw_limiter}"),
204 )
205 .into_response())
206 })
207 }
208 }
209
210 impl NamedService for EchoService {
211 const NAME: &'static str = "test.Echo";
212 }
213
214 #[tokio::test]
215 async fn test_inserts_limiter_for_uncompressed_requests() {
216 let limiter = ServerMemoryLimiter::new(1024 * 1024, OnExhaustedPolicy::Fail);
217 let mut svc =
218 MemoryLimiterExtensionLayer::new(limiter.clone(), 512 * 1024 * 1024).layer(EchoService);
219
220 let req = Request::builder().body(Body::empty()).unwrap();
221 let res = svc.ready().await.unwrap().call(req).await.unwrap();
222 assert_eq!(res.status(), StatusCode::OK);
223 let body = axum::body::to_bytes(res.into_body(), 1024).await.unwrap();
224 assert_eq!(
225 &body[..],
226 &b"reservation=false limiter=true"[..],
227 "uncompressed requests must not be pre-reserved"
228 );
229 assert_eq!(0, limiter.used_bytes());
230 }
231
232 #[tokio::test]
233 async fn test_reserves_max_message_size_for_compressed_requests() {
234 let max_size = 512 * 1024 * 1024;
235 let limiter = ServerMemoryLimiter::new(1024 * 1024 * 1024, OnExhaustedPolicy::Fail);
236 let mut svc =
237 MemoryLimiterExtensionLayer::new(limiter.clone(), max_size).layer(EchoService);
238
239 let req = Request::builder()
243 .header(GRPC_ENCODING_HEADER, HeaderValue::from_static("gzip"))
244 .body(Body::empty())
245 .unwrap();
246 let res = svc.ready().await.unwrap().call(req).await.unwrap();
247 assert_eq!(res.status(), StatusCode::OK);
248 let body = axum::body::to_bytes(res.into_body(), 1024).await.unwrap();
249 assert_eq!(
250 &body[..],
251 &b"reservation=true limiter=true"[..],
252 "compressed requests must carry a pre-decode reservation"
253 );
254 assert_eq!(0, limiter.used_bytes());
256 }
257
258 #[tokio::test]
259 async fn test_rejects_compressed_request_when_quota_exhausted() {
260 let limiter = ServerMemoryLimiter::new(1024, OnExhaustedPolicy::Fail);
262 let mut svc =
263 MemoryLimiterExtensionLayer::new(limiter.clone(), 512 * 1024 * 1024).layer(EchoService);
264
265 let req = Request::builder()
266 .header(GRPC_ENCODING_HEADER, HeaderValue::from_static("zstd"))
267 .body(Body::empty())
268 .unwrap();
269 let res = svc.ready().await.unwrap().call(req).await.unwrap();
270 assert_eq!(res.status(), StatusCode::OK);
271 assert_eq!(
273 res.headers()
274 .get("grpc-status")
275 .and_then(|v| v.to_str().ok()),
276 Some("8"),
277 "expected RESOURCE_EXHAUSTED grpc status"
278 );
279 }
280
281 #[tokio::test]
282 async fn test_identity_encoding_is_not_reserved() {
283 let limiter = ServerMemoryLimiter::new(1024 * 1024, OnExhaustedPolicy::Fail);
284 let mut svc =
285 MemoryLimiterExtensionLayer::new(limiter.clone(), 512 * 1024 * 1024).layer(EchoService);
286
287 let req = Request::builder()
288 .header(
289 GRPC_ENCODING_HEADER,
290 HeaderValue::from_static(IDENTITY_ENCODING),
291 )
292 .body(Body::empty())
293 .unwrap();
294 let res = svc.ready().await.unwrap().call(req).await.unwrap();
295 let body = axum::body::to_bytes(res.into_body(), 1024).await.unwrap();
296 assert_eq!(&body[..], &b"reservation=false limiter=true"[..]);
297 }
298}