1use std::{
9 future::Future,
10 pin::Pin,
11 task::{Context, Poll, ready},
12 time::Duration,
13};
14
15use http::{HeaderMap, HeaderValue, Request, Response};
16use pin_project_lite::pin_project;
17use tokio::time::Sleep;
18use tonic::Status;
19use tower::Service;
20
21const GRPC_TIMEOUT_HEADER: &str = "grpc-timeout";
22
23#[derive(Debug, Clone)]
29pub struct GrpcTimeout<S> {
30 inner: S,
31 server_timeout: Option<Duration>,
32 timeout_exempt_paths: &'static [&'static str],
36}
37
38impl<S> GrpcTimeout<S> {
39 pub fn new(inner: S, server_timeout: Option<Duration>) -> Self {
40 Self::new_with_exempt_paths(inner, server_timeout, &[])
41 }
42
43 pub fn new_with_exempt_paths(
44 inner: S,
45 server_timeout: Option<Duration>,
46 timeout_exempt_paths: &'static [&'static str],
47 ) -> Self {
48 Self {
49 inner,
50 server_timeout,
51 timeout_exempt_paths,
52 }
53 }
54}
55
56impl<S, RequestBody, ResponseBody> Service<Request<RequestBody>> for GrpcTimeout<S>
57where
58 S: Service<Request<RequestBody>, Response = Response<ResponseBody>>,
59 ResponseBody: Default,
60{
61 type Response = Response<ResponseBody>;
62 type Error = S::Error;
63 type Future = ResponseFuture<S::Future>;
64
65 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
66 self.inner.poll_ready(cx).map_err(Into::into)
67 }
68
69 fn call(&mut self, req: Request<RequestBody>) -> Self::Future {
70 let client_timeout = try_parse_grpc_timeout(req.headers()).unwrap_or_else(|e| {
71 tracing::trace!("Error parsing `grpc-timeout` header {:?}", e);
72 None
73 });
74
75 let server_timeout = if self.timeout_exempt_paths.contains(&req.uri().path()) {
78 None
79 } else {
80 self.server_timeout
81 };
82
83 let timeout_duration = match (client_timeout, server_timeout) {
85 (None, None) => None,
86 (Some(dur), None) => Some(dur),
87 (None, Some(dur)) => Some(dur),
88 (Some(header), Some(server)) => {
89 let shorter_duration = std::cmp::min(header, server);
90 Some(shorter_duration)
91 }
92 };
93
94 ResponseFuture {
95 inner: self.inner.call(req),
96 sleep: timeout_duration.map(tokio::time::sleep),
97 }
98 }
99}
100
101pin_project! {
102 pub struct ResponseFuture<F> {
103 #[pin]
104 inner: F,
105 #[pin]
106 sleep: Option<Sleep>,
107 }
108}
109
110impl<F, ResponseBody, E> Future for ResponseFuture<F>
111where
112 F: Future<Output = Result<Response<ResponseBody>, E>>,
113 ResponseBody: Default,
114{
115 type Output = F::Output;
116
117 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
118 let this = self.project();
119
120 if let Poll::Ready(result) = this.inner.poll(cx) {
121 return Poll::Ready(result);
122 }
123
124 if let Some(sleep) = this.sleep.as_pin_mut() {
125 ready!(sleep.poll(cx));
126 return Poll::Ready(Ok(Status::deadline_exceeded("Timeout expired").into_http()));
127 }
128
129 Poll::Pending
130 }
131}
132
133const SECONDS_IN_HOUR: u64 = 60 * 60;
134const SECONDS_IN_MINUTE: u64 = 60;
135
136fn try_parse_grpc_timeout(
141 headers: &HeaderMap<HeaderValue>,
142) -> Result<Option<Duration>, &HeaderValue> {
143 let Some(val) = headers.get(GRPC_TIMEOUT_HEADER) else {
144 return Ok(None);
145 };
146
147 let (timeout_value, timeout_unit) = val
148 .to_str()
149 .map_err(|_| val)
150 .and_then(|s| if s.is_empty() { Err(val) } else { Ok(s) })?
151 .split_at(val.len() - 1);
157
158 if timeout_value.len() > 8 {
161 return Err(val);
162 }
163
164 let timeout_value: u64 = timeout_value.parse().map_err(|_| val)?;
165
166 let duration = match timeout_unit {
167 "H" => Duration::from_secs(timeout_value * SECONDS_IN_HOUR),
169 "M" => Duration::from_secs(timeout_value * SECONDS_IN_MINUTE),
171 "S" => Duration::from_secs(timeout_value),
173 "m" => Duration::from_millis(timeout_value),
175 "u" => Duration::from_micros(timeout_value),
177 "n" => Duration::from_nanos(timeout_value),
179 _ => return Err(val),
180 };
181
182 Ok(Some(duration))
183}
184
185#[cfg(test)]
186mod tests {
187 use std::{convert::Infallible, future, future::Pending, marker::PhantomData};
188
189 use http_body_util::BodyExt;
190 use tonic::body::Body;
191
192 use super::*;
193 use crate::concurrency::ReclaimableBody;
194
195 fn setup_map_try_parse(val: Option<&str>) -> Result<Option<Duration>, HeaderValue> {
197 let mut hm = HeaderMap::new();
198 if let Some(v) = val {
199 let hv = HeaderValue::from_str(v).unwrap();
200 hm.insert(GRPC_TIMEOUT_HEADER, hv);
201 };
202
203 try_parse_grpc_timeout(&hm).map_err(|e| e.clone())
204 }
205
206 #[test]
207 fn test_hours() {
208 let parsed_duration = setup_map_try_parse(Some("3H")).unwrap().unwrap();
209 assert_eq!(Duration::from_secs(3 * 60 * 60), parsed_duration);
210 }
211
212 #[test]
213 fn test_minutes() {
214 let parsed_duration = setup_map_try_parse(Some("1M")).unwrap().unwrap();
215 assert_eq!(Duration::from_secs(60), parsed_duration);
216 }
217
218 #[test]
219 fn test_seconds() {
220 let parsed_duration = setup_map_try_parse(Some("42S")).unwrap().unwrap();
221 assert_eq!(Duration::from_secs(42), parsed_duration);
222 }
223
224 #[test]
225 fn test_milliseconds() {
226 let parsed_duration = setup_map_try_parse(Some("13m")).unwrap().unwrap();
227 assert_eq!(Duration::from_millis(13), parsed_duration);
228 }
229
230 #[test]
231 fn test_microseconds() {
232 let parsed_duration = setup_map_try_parse(Some("2u")).unwrap().unwrap();
233 assert_eq!(Duration::from_micros(2), parsed_duration);
234 }
235
236 #[test]
237 fn test_nanoseconds() {
238 let parsed_duration = setup_map_try_parse(Some("82n")).unwrap().unwrap();
239 assert_eq!(Duration::from_nanos(82), parsed_duration);
240 }
241
242 #[test]
243 fn test_header_not_present() {
244 let parsed_duration = setup_map_try_parse(None).unwrap();
245 assert!(parsed_duration.is_none());
246 }
247
248 #[test]
249 #[should_panic(expected = "82f")]
250 fn test_invalid_unit() {
251 setup_map_try_parse(Some("82f")).unwrap().unwrap();
253 }
254
255 #[test]
256 #[should_panic(expected = "123456789H")]
257 fn test_too_many_digits() {
258 setup_map_try_parse(Some("123456789H")).unwrap().unwrap();
260 }
261
262 #[test]
263 #[should_panic(expected = "oneH")]
264 fn test_invalid_digits() {
265 setup_map_try_parse(Some("oneH")).unwrap().unwrap();
267 }
268
269 struct NeverResponds<B>(PhantomData<B>);
272
273 impl<B> NeverResponds<B> {
274 fn new() -> Self {
275 Self(PhantomData)
276 }
277 }
278
279 impl<B> Service<Request<()>> for NeverResponds<B> {
280 type Response = Response<B>;
281 type Error = Infallible;
282 type Future = Pending<Result<Response<B>, Infallible>>;
283
284 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
285 Poll::Ready(Ok(()))
286 }
287
288 fn call(&mut self, _req: Request<()>) -> Self::Future {
289 future::pending()
290 }
291 }
292
293 #[tokio::test]
295 async fn a_call_past_its_deadline_ends_with_deadline_exceeded_and_an_empty_body() {
296 async fn assert_deadline_response<B>()
297 where
298 B: http_body::Body + Default,
299 B::Error: std::fmt::Debug,
300 {
301 let mut svc =
302 GrpcTimeout::new(NeverResponds::<B>::new(), Some(Duration::from_millis(10)));
303 let response = tokio::time::timeout(Duration::from_secs(5), svc.call(Request::new(())))
304 .await
305 .expect("the call must end at its deadline")
306 .unwrap();
307 assert_eq!(
308 Status::from_header_map(response.headers()).map(|status| status.code()),
309 Some(tonic::Code::DeadlineExceeded)
310 );
311 assert!(response.body().is_end_stream());
312 assert_eq!(response.body().size_hint().exact(), Some(0));
313 let body = response.into_body().collect().await.unwrap();
314 assert!(body.trailers().is_none());
315 assert!(body.to_bytes().is_empty());
316 }
317
318 assert_deadline_response::<Body>().await;
319 assert_deadline_response::<tonic::service::AxumBody>().await;
320 assert_deadline_response::<ReclaimableBody<Body, (), ()>>().await;
321 }
322
323 #[tokio::test]
324 async fn server_timeout_skips_exempt_paths() {
325 const EXEMPT: &[&str] = &["/pkg.Svc/Stream"];
326 let timeout = Duration::from_millis(50);
327
328 let mut svc =
330 GrpcTimeout::new_with_exempt_paths(NeverResponds::<Body>::new(), Some(timeout), EXEMPT);
331 let fut = svc.call(Request::builder().uri("/pkg.Svc/Unary").body(()).unwrap());
332 assert!(
333 fut.await.is_ok(),
334 "non-exempt request should be completed by the fallback timeout"
335 );
336
337 let mut svc =
339 GrpcTimeout::new_with_exempt_paths(NeverResponds::<Body>::new(), Some(timeout), EXEMPT);
340 let fut = svc.call(Request::builder().uri("/pkg.Svc/Stream").body(()).unwrap());
341 assert!(
342 tokio::time::timeout(Duration::from_millis(150), fut)
343 .await
344 .is_err(),
345 "exempt path must not be aborted by the server timeout"
346 );
347 }
348}