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, body::Body};
19use tower::Service;
20
21const GRPC_TIMEOUT_HEADER: &str = "grpc-timeout";
22
23#[derive(Debug, Clone)]
24pub struct GrpcTimeout<S> {
25 inner: S,
26 server_timeout: Option<Duration>,
27 timeout_exempt_paths: &'static [&'static str],
31}
32
33impl<S> GrpcTimeout<S> {
34 pub fn new(inner: S, server_timeout: Option<Duration>) -> Self {
35 Self::new_with_exempt_paths(inner, server_timeout, &[])
36 }
37
38 pub fn new_with_exempt_paths(
39 inner: S,
40 server_timeout: Option<Duration>,
41 timeout_exempt_paths: &'static [&'static str],
42 ) -> Self {
43 Self {
44 inner,
45 server_timeout,
46 timeout_exempt_paths,
47 }
48 }
49}
50
51impl<S, RequestBody, ResponseBody> Service<Request<RequestBody>> for GrpcTimeout<S>
52where
53 S: Service<Request<RequestBody>, Response = Response<ResponseBody>>,
54{
55 type Response = Response<MaybeEmptyBody<ResponseBody>>;
56 type Error = S::Error;
57 type Future = ResponseFuture<S::Future>;
58
59 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
60 self.inner.poll_ready(cx).map_err(Into::into)
61 }
62
63 fn call(&mut self, req: Request<RequestBody>) -> Self::Future {
64 let client_timeout = try_parse_grpc_timeout(req.headers()).unwrap_or_else(|e| {
65 tracing::trace!("Error parsing `grpc-timeout` header {:?}", e);
66 None
67 });
68
69 let server_timeout = if self.timeout_exempt_paths.contains(&req.uri().path()) {
72 None
73 } else {
74 self.server_timeout
75 };
76
77 let timeout_duration = match (client_timeout, server_timeout) {
79 (None, None) => None,
80 (Some(dur), None) => Some(dur),
81 (None, Some(dur)) => Some(dur),
82 (Some(header), Some(server)) => {
83 let shorter_duration = std::cmp::min(header, server);
84 Some(shorter_duration)
85 }
86 };
87
88 ResponseFuture {
89 inner: self.inner.call(req),
90 sleep: timeout_duration.map(tokio::time::sleep),
91 }
92 }
93}
94
95pin_project! {
96 pub struct ResponseFuture<F> {
97 #[pin]
98 inner: F,
99 #[pin]
100 sleep: Option<Sleep>,
101 }
102}
103
104impl<F, ResponseBody, E> Future for ResponseFuture<F>
105where
106 F: Future<Output = Result<Response<ResponseBody>, E>>,
107{
108 type Output = Result<Response<MaybeEmptyBody<ResponseBody>>, E>;
109
110 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
111 let this = self.project();
112
113 if let Poll::Ready(result) = this.inner.poll(cx) {
114 return Poll::Ready(result.map(|response| response.map(MaybeEmptyBody::full)));
115 }
116
117 if let Some(sleep) = this.sleep.as_pin_mut() {
118 ready!(sleep.poll(cx));
119 let response = Status::deadline_exceeded("Timeout expired")
120 .into_http()
121 .map(|_: Body| MaybeEmptyBody::empty());
122 return Poll::Ready(Ok(response));
123 }
124
125 Poll::Pending
126 }
127}
128
129pin_project! {
130 pub struct MaybeEmptyBody<B> {
131 #[pin]
132 inner: Option<B>,
133 }
134}
135
136impl<B> MaybeEmptyBody<B> {
137 fn full(inner: B) -> Self {
138 Self { inner: Some(inner) }
139 }
140
141 fn empty() -> Self {
142 Self { inner: None }
143 }
144}
145
146impl<B> http_body::Body for MaybeEmptyBody<B>
147where
148 B: http_body::Body + Send,
149{
150 type Data = B::Data;
151 type Error = B::Error;
152
153 fn poll_frame(
154 self: Pin<&mut Self>,
155 cx: &mut Context<'_>,
156 ) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
157 match self.project().inner.as_pin_mut() {
158 Some(b) => b.poll_frame(cx),
159 None => Poll::Ready(None),
160 }
161 }
162
163 fn is_end_stream(&self) -> bool {
164 match &self.inner {
165 Some(b) => b.is_end_stream(),
166 None => true,
167 }
168 }
169
170 fn size_hint(&self) -> http_body::SizeHint {
171 match &self.inner {
172 Some(body) => body.size_hint(),
173 None => http_body::SizeHint::with_exact(0),
174 }
175 }
176}
177
178const SECONDS_IN_HOUR: u64 = 60 * 60;
179const SECONDS_IN_MINUTE: u64 = 60;
180
181fn try_parse_grpc_timeout(
186 headers: &HeaderMap<HeaderValue>,
187) -> Result<Option<Duration>, &HeaderValue> {
188 let Some(val) = headers.get(GRPC_TIMEOUT_HEADER) else {
189 return Ok(None);
190 };
191
192 let (timeout_value, timeout_unit) = val
193 .to_str()
194 .map_err(|_| val)
195 .and_then(|s| if s.is_empty() { Err(val) } else { Ok(s) })?
196 .split_at(val.len() - 1);
202
203 if timeout_value.len() > 8 {
206 return Err(val);
207 }
208
209 let timeout_value: u64 = timeout_value.parse().map_err(|_| val)?;
210
211 let duration = match timeout_unit {
212 "H" => Duration::from_secs(timeout_value * SECONDS_IN_HOUR),
214 "M" => Duration::from_secs(timeout_value * SECONDS_IN_MINUTE),
216 "S" => Duration::from_secs(timeout_value),
218 "m" => Duration::from_millis(timeout_value),
220 "u" => Duration::from_micros(timeout_value),
222 "n" => Duration::from_nanos(timeout_value),
224 _ => return Err(val),
225 };
226
227 Ok(Some(duration))
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn setup_map_try_parse(val: Option<&str>) -> Result<Option<Duration>, HeaderValue> {
236 let mut hm = HeaderMap::new();
237 if let Some(v) = val {
238 let hv = HeaderValue::from_str(v).unwrap();
239 hm.insert(GRPC_TIMEOUT_HEADER, hv);
240 };
241
242 try_parse_grpc_timeout(&hm).map_err(|e| e.clone())
243 }
244
245 #[test]
246 fn test_hours() {
247 let parsed_duration = setup_map_try_parse(Some("3H")).unwrap().unwrap();
248 assert_eq!(Duration::from_secs(3 * 60 * 60), parsed_duration);
249 }
250
251 #[test]
252 fn test_minutes() {
253 let parsed_duration = setup_map_try_parse(Some("1M")).unwrap().unwrap();
254 assert_eq!(Duration::from_secs(60), parsed_duration);
255 }
256
257 #[test]
258 fn test_seconds() {
259 let parsed_duration = setup_map_try_parse(Some("42S")).unwrap().unwrap();
260 assert_eq!(Duration::from_secs(42), parsed_duration);
261 }
262
263 #[test]
264 fn test_milliseconds() {
265 let parsed_duration = setup_map_try_parse(Some("13m")).unwrap().unwrap();
266 assert_eq!(Duration::from_millis(13), parsed_duration);
267 }
268
269 #[test]
270 fn test_microseconds() {
271 let parsed_duration = setup_map_try_parse(Some("2u")).unwrap().unwrap();
272 assert_eq!(Duration::from_micros(2), parsed_duration);
273 }
274
275 #[test]
276 fn test_nanoseconds() {
277 let parsed_duration = setup_map_try_parse(Some("82n")).unwrap().unwrap();
278 assert_eq!(Duration::from_nanos(82), parsed_duration);
279 }
280
281 #[test]
282 fn test_header_not_present() {
283 let parsed_duration = setup_map_try_parse(None).unwrap();
284 assert!(parsed_duration.is_none());
285 }
286
287 #[test]
288 #[should_panic(expected = "82f")]
289 fn test_invalid_unit() {
290 setup_map_try_parse(Some("82f")).unwrap().unwrap();
292 }
293
294 #[test]
295 #[should_panic(expected = "123456789H")]
296 fn test_too_many_digits() {
297 setup_map_try_parse(Some("123456789H")).unwrap().unwrap();
299 }
300
301 #[test]
302 #[should_panic(expected = "oneH")]
303 fn test_invalid_digits() {
304 setup_map_try_parse(Some("oneH")).unwrap().unwrap();
306 }
307
308 #[tokio::test]
309 async fn server_timeout_skips_exempt_paths() {
310 use std::{convert::Infallible, future, future::Pending};
311
312 #[derive(Clone)]
315 struct NeverReady;
316 impl Service<Request<()>> for NeverReady {
317 type Response = Response<Body>;
318 type Error = Infallible;
319 type Future = Pending<Result<Response<Body>, Infallible>>;
320
321 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
322 Poll::Ready(Ok(()))
323 }
324
325 fn call(&mut self, _req: Request<()>) -> Self::Future {
326 future::pending()
327 }
328 }
329
330 const EXEMPT: &[&str] = &["/pkg.Svc/Stream"];
331 let timeout = Duration::from_millis(50);
332
333 let mut svc = GrpcTimeout::new_with_exempt_paths(NeverReady, Some(timeout), EXEMPT);
335 let fut = svc.call(Request::builder().uri("/pkg.Svc/Unary").body(()).unwrap());
336 assert!(
337 fut.await.is_ok(),
338 "non-exempt request should be completed by the fallback timeout"
339 );
340
341 let mut svc = GrpcTimeout::new_with_exempt_paths(NeverReady, Some(timeout), EXEMPT);
343 let fut = svc.call(Request::builder().uri("/pkg.Svc/Stream").body(()).unwrap());
344 assert!(
345 tokio::time::timeout(Duration::from_millis(150), fut)
346 .await
347 .is_err(),
348 "exempt path must not be aborted by the server timeout"
349 );
350 }
351}