1use std::any::Any;
2use std::fmt;
3use std::future::Future;
4use std::pin::{pin, Pin};
5use std::sync::Arc;
6use std::task::{Context, Poll};
7use std::time::Duration;
8
9use bytes::Bytes;
10use futures::future::{select, Either};
11use hyper::body::{Body, Frame, SizeHint};
12use hyper::header::{HeaderMap, HeaderName, HeaderValue};
13use hyper::Uri;
14use tokio::time::{sleep, sleep_until, Instant, Sleep};
15use tonic::Status;
16use tracing::error;
17
18const GRPC_TIMEOUT: HeaderName = HeaderName::from_static("grpc-timeout");
19
20const MAX_GRPC_TIMEOUT_VALUE: u128 = 99_999_999;
22
23const GRPC_TIMEOUT_UNITS: [(char, u128); 6] = [
25 ('n', 1),
26 ('u', 1_000),
27 ('m', 1_000_000),
28 ('S', 1_000_000_000),
29 ('M', 60 * 1_000_000_000),
30 ('H', 60 * 60 * 1_000_000_000),
31];
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
60pub struct RequestTimeouts {
61 total: Option<Duration>,
62 idle: Option<Duration>,
63}
64
65impl RequestTimeouts {
66 pub const fn new() -> Self {
68 Self {
69 total: None,
70 idle: None,
71 }
72 }
73
74 pub const fn with_total(self, total: Duration) -> Self {
81 Self {
82 total: Some(total),
83 ..self
84 }
85 }
86
87 pub const fn with_idle(self, idle: Duration) -> Self {
94 Self {
95 idle: Some(idle),
96 ..self
97 }
98 }
99
100 pub(crate) fn start(self) -> Option<CallClock> {
101 if self.total.is_none() && self.idle.is_none() {
102 return None;
103 }
104 let started = Instant::now();
105 Some(CallClock {
106 started,
107 total: self
108 .total
109 .and_then(|limit| Some((limit, started.checked_add(limit)?))),
110 idle: self.idle,
111 })
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum RequestTimeoutElapsed {
120 Total(Duration),
122 Idle(Duration),
124}
125
126impl fmt::Display for RequestTimeoutElapsed {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Self::Total(limit) => write!(f, "total request timeout of {limit:?} elapsed"),
130 Self::Idle(limit) => write!(f, "idle request timeout of {limit:?} elapsed"),
131 }
132 }
133}
134
135impl std::error::Error for RequestTimeoutElapsed {}
136
137impl From<RequestTimeoutElapsed> for Status {
138 fn from(elapsed: RequestTimeoutElapsed) -> Self {
139 let mut status = Status::deadline_exceeded(elapsed.to_string());
140 status.set_source(Arc::new(elapsed));
141 status
142 }
143}
144
145#[derive(Clone, Copy)]
148pub(crate) struct CallClock {
149 started: Instant,
150 total: Option<(Duration, Instant)>,
152 idle: Option<Duration>,
153}
154
155impl CallClock {
156 pub(crate) fn insert_grpc_timeout(&self, headers: &mut HeaderMap) -> Result<(), Status> {
159 let Some((limit, deadline)) = self.total else {
160 return Ok(());
161 };
162 let remaining = deadline.saturating_duration_since(Instant::now());
163 if remaining.is_zero() {
164 return Err(RequestTimeoutElapsed::Total(limit).into());
165 }
166 if let Some(value) = grpc_timeout_value(remaining) {
167 headers.entry(GRPC_TIMEOUT).or_insert(value);
168 }
169 Ok(())
170 }
171
172 pub(crate) async fn limit<B: 'static, E: From<Status>>(
175 self,
176 uri: Uri,
177 send: impl Future<Output = Result<hyper::Response<B>, E>>,
178 ) -> Result<hyper::Response<B>, E> {
179 let total = self
180 .total
181 .map(|(limit, deadline)| (deadline, RequestTimeoutElapsed::Total(limit)));
182 let idle = self.idle.and_then(|limit| {
183 Some((
184 self.started.checked_add(limit)?,
185 RequestTimeoutElapsed::Idle(limit),
186 ))
187 });
188 let response = match total.into_iter().chain(idle).min_by_key(|(at, _)| *at) {
189 Some((deadline, elapsed)) => {
193 match select(pin!(sleep_until(deadline)), pin!(send)).await {
194 Either::Left(_) => {
195 error!(
196 req_uri = %uri,
197 "Err: took {}ms ({elapsed})",
198 self.started.elapsed().as_millis()
199 );
200 return Err(Status::from(elapsed).into());
201 }
202 Either::Right((response, _)) => response?,
203 }
204 }
205 None => send.await?,
206 };
207 Ok(response.map(|body| self.limit_body(body)))
208 }
209
210 fn limit_body<B: 'static>(self, mut body: B) -> B {
213 if let Some(body) = (&mut body as &mut dyn Any).downcast_mut::<tonic::body::Body>() {
214 *body = tonic::body::Body::new(TimeoutBody {
215 inner: std::mem::take(body),
216 total: self
217 .total
218 .map(|(limit, deadline)| (limit, Box::pin(sleep_until(deadline)))),
219 idle: self.idle.map(|limit| IdleTimer {
220 limit,
221 sleep: Box::pin(sleep(limit)),
222 waiting: false,
223 }),
224 });
225 }
226 body
227 }
228}
229
230fn grpc_timeout_value(timeout: Duration) -> Option<HeaderValue> {
234 let nanos = timeout.as_nanos();
235 let (value, unit) = GRPC_TIMEOUT_UNITS
236 .iter()
237 .map(|&(unit, unit_nanos)| (nanos.div_ceil(unit_nanos), unit))
238 .find(|&(value, _)| value <= MAX_GRPC_TIMEOUT_VALUE)?;
239 HeaderValue::try_from(format!("{value}{unit}")).ok()
240}
241
242struct TimeoutBody {
243 inner: tonic::body::Body,
244 total: Option<(Duration, Pin<Box<Sleep>>)>,
246 idle: Option<IdleTimer>,
247}
248
249struct IdleTimer {
252 limit: Duration,
253 sleep: Pin<Box<Sleep>>,
254 waiting: bool,
255}
256
257impl Body for TimeoutBody {
258 type Data = Bytes;
259 type Error = Status;
260
261 fn poll_frame(
262 self: Pin<&mut Self>,
263 cx: &mut Context<'_>,
264 ) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
265 let this = self.get_mut();
266 if let Some((limit, deadline)) = &mut this.total {
267 if deadline.as_mut().poll(cx).is_ready() {
268 return Poll::Ready(Some(Err(RequestTimeoutElapsed::Total(*limit).into())));
269 }
270 }
271 if let Some(idle) = &mut this.idle {
272 if !idle.waiting {
273 if let Some(deadline) = Instant::now().checked_add(idle.limit) {
276 idle.sleep.as_mut().reset(deadline);
277 }
278 idle.waiting = true;
279 }
280 }
281 match Pin::new(&mut this.inner).poll_frame(cx) {
282 Poll::Ready(None) => {
283 this.total = None;
285 this.idle = None;
286 Poll::Ready(None)
287 }
288 Poll::Ready(frame) => {
289 if let Some(idle) = &mut this.idle {
290 idle.waiting = false;
291 }
292 Poll::Ready(frame)
293 }
294 Poll::Pending => {
295 if let Some(idle) = &mut this.idle {
296 if idle.sleep.as_mut().poll(cx).is_ready() {
297 return Poll::Ready(Some(Err(
298 RequestTimeoutElapsed::Idle(idle.limit).into()
299 )));
300 }
301 }
302 Poll::Pending
303 }
304 }
305 }
306
307 fn is_end_stream(&self) -> bool {
308 self.inner.is_end_stream()
309 }
310
311 fn size_hint(&self) -> SizeHint {
312 self.inner.size_hint()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::middleware::GoogleAuthMiddlewareService;
320 use crate::test_support::{silent_endpoint, stalling_stream_channel, StubCredentials};
321 use crate::{GoogleApi, GoogleAuthHeaders, GoogleAuthMiddleware};
322 use google_cloud_auth::credentials::Credentials;
323 use hyper::http::uri::PathAndQuery;
324 use std::error::Error as _;
325 use std::future::poll_fn;
326 use tokio::time::Instant;
327 use tonic::client::Grpc;
328 use tonic::codec::Streaming;
329 use tonic::Code;
330 use tonic_prost::ProstCodec;
331
332 const LIMIT: Duration = Duration::from_secs(5);
333
334 async fn unary(
335 service: GoogleAuthMiddleware,
336 request: tonic::Request<()>,
337 ) -> Result<tonic::Response<()>, Status> {
338 let mut grpc = Grpc::new(service);
339 grpc.ready().await.unwrap();
340 grpc.unary(
341 request,
342 PathAndQuery::from_static("/test.Silent/Call"),
343 ProstCodec::default(),
344 )
345 .await
346 }
347
348 async fn open_stream(
349 service: GoogleAuthMiddleware,
350 request: tonic::Request<()>,
351 ) -> Streaming<()> {
352 let mut grpc = Grpc::new(service);
353 grpc.ready().await.unwrap();
354 grpc.server_streaming(
355 request,
356 PathAndQuery::from_static("/test.StallingStream/Stream"),
357 ProstCodec::default(),
358 )
359 .await
360 .unwrap()
361 .into_inner()
362 }
363
364 #[tokio::test(start_paused = true)]
365 async fn total_limit_fails_a_call_the_server_never_answers() {
366 let (url, _) = silent_endpoint().await;
367 let client: GoogleApi<GoogleAuthMiddleware> = GoogleApi::from_function_with_credentials(
368 |service| service,
369 url,
370 None,
371 Credentials::from(StubCredentials::bearer("token")),
372 )
373 .await
374 .unwrap()
375 .with_request_timeouts(RequestTimeouts::new().with_total(LIMIT));
376
377 let started = Instant::now();
378 let status = unary(client.get(), tonic::Request::new(()))
379 .await
380 .unwrap_err();
381
382 assert_eq!(started.elapsed(), LIMIT);
383 assert_eq!(status.code(), Code::DeadlineExceeded);
384 assert_eq!(
385 status.source().and_then(|source| source.downcast_ref()),
386 Some(&RequestTimeoutElapsed::Total(LIMIT))
387 );
388 }
389
390 #[tokio::test(start_paused = true)]
393 async fn total_limit_past_milliseconds_fails_with_deadline_exceeded() {
394 let limit = Duration::from_micros(110_000_500);
395 let (url, _) = silent_endpoint().await;
396 let client: GoogleApi<GoogleAuthMiddleware> = GoogleApi::from_function_with_credentials(
397 |service| service,
398 url,
399 None,
400 Credentials::from(StubCredentials::bearer("token")),
401 )
402 .await
403 .unwrap()
404 .with_request_timeouts(RequestTimeouts::new().with_total(limit));
405
406 let started = Instant::now();
407 let status = unary(client.get(), tonic::Request::new(()))
408 .await
409 .unwrap_err();
410
411 assert!((limit..limit + Duration::from_millis(1)).contains(&started.elapsed()));
413 assert_eq!(status.code(), Code::DeadlineExceeded);
414 assert_eq!(
415 status.source().and_then(|source| source.downcast_ref()),
416 Some(&RequestTimeoutElapsed::Total(limit))
417 );
418 }
419
420 #[tokio::test(start_paused = true)]
421 async fn total_limit_ends_a_stream_that_keeps_delivering() {
422 let service = GoogleAuthMiddlewareService::new(
423 stalling_stream_channel(vec![Duration::from_secs(2); 10]).await,
424 GoogleAuthHeaders::from(Credentials::from(StubCredentials::bearer("token"))),
425 None,
426 )
427 .unwrap();
428 let mut request = tonic::Request::new(());
429 request
430 .extensions_mut()
431 .insert(RequestTimeouts::new().with_total(LIMIT));
432
433 let started = Instant::now();
434 let mut messages = open_stream(service, request).await;
435 let status = tokio::time::timeout(LIMIT * 2, async {
436 while messages.message().await?.is_some() {}
437 Ok::<_, Status>(())
438 })
439 .await
440 .expect("the total limit ends the stream")
441 .unwrap_err();
442
443 assert_eq!(started.elapsed(), LIMIT);
444 assert_eq!(status.code(), Code::DeadlineExceeded);
445 assert_eq!(
446 status.source().and_then(|source| source.downcast_ref()),
447 Some(&RequestTimeoutElapsed::Total(LIMIT))
448 );
449 }
450
451 #[tokio::test(start_paused = true)]
453 async fn limits_end_with_the_response_body() {
454 let clock = RequestTimeouts::new()
455 .with_total(LIMIT)
456 .with_idle(LIMIT)
457 .start()
458 .unwrap();
459 let mut body = clock.limit_body(tonic::body::Body::new(String::from("message")));
460
461 assert!(poll_fn(|cx| Pin::new(&mut body).poll_frame(cx))
462 .await
463 .is_some_and(|frame| frame.is_ok()));
464 assert!(poll_fn(|cx| Pin::new(&mut body).poll_frame(cx))
465 .await
466 .is_none());
467 tokio::time::sleep(LIMIT * 2).await;
468 assert!(poll_fn(|cx| Pin::new(&mut body).poll_frame(cx))
469 .await
470 .is_none());
471 }
472
473 #[tokio::test(start_paused = true)]
474 async fn idle_limit_fails_a_stream_that_stops_after_its_first_message() {
475 let service = GoogleAuthMiddlewareService::new(
476 stalling_stream_channel(vec![Duration::ZERO]).await,
477 GoogleAuthHeaders::from(Credentials::from(StubCredentials::bearer("token"))),
478 None,
479 )
480 .unwrap();
481 let mut request = tonic::Request::new(());
482 request
483 .extensions_mut()
484 .insert(RequestTimeouts::new().with_idle(LIMIT));
485 let mut messages = open_stream(service, request).await;
486 assert_eq!(messages.message().await.unwrap(), Some(()));
487
488 let started = Instant::now();
489 let status = tokio::time::timeout(LIMIT * 2, messages.message())
490 .await
491 .expect("the idle limit ends the wait")
492 .unwrap_err();
493
494 assert_eq!(started.elapsed(), LIMIT);
495 assert_eq!(status.code(), Code::DeadlineExceeded);
496 assert_eq!(
497 status.source().and_then(|source| source.downcast_ref()),
498 Some(&RequestTimeoutElapsed::Idle(LIMIT))
499 );
500 }
501
502 #[tokio::test(start_paused = true)]
506 async fn idle_limit_spares_a_slow_stream_and_a_slow_reader() {
507 let gaps = [3, 3, 7].map(Duration::from_secs).to_vec();
508 let mut service = GoogleAuthMiddlewareService::new(
509 stalling_stream_channel(gaps).await,
510 GoogleAuthHeaders::from(Credentials::from(StubCredentials::bearer("token"))),
511 None,
512 )
513 .unwrap();
514 service.set_request_timeouts(RequestTimeouts::new().with_total(LIMIT));
515 let mut request = tonic::Request::new(());
516 request
517 .extensions_mut()
518 .insert(RequestTimeouts::new().with_idle(LIMIT));
519 let mut messages = open_stream(service, request).await;
520
521 assert_eq!(messages.message().await.unwrap(), Some(()));
522 assert_eq!(messages.message().await.unwrap(), Some(()));
523 tokio::time::sleep(Duration::from_secs(4)).await;
524 assert_eq!(messages.message().await.unwrap(), Some(()));
525 }
526
527 #[tokio::test(start_paused = true)]
530 async fn without_limits_a_call_waits_for_the_connection() {
531 let (url, _) = silent_endpoint().await;
532 let client: GoogleApi<GoogleAuthMiddleware> = GoogleApi::from_function_with_credentials(
533 |service| service,
534 url,
535 None,
536 Credentials::from(StubCredentials::bearer("token")),
537 )
538 .await
539 .unwrap();
540
541 let call = unary(client.get(), tonic::Request::new(()));
542
543 assert!(tokio::time::timeout(Duration::from_secs(60), call)
544 .await
545 .is_err());
546 }
547}