use std::any::Any;
use std::fmt;
use std::future::Future;
use std::pin::{pin, Pin};
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use bytes::Bytes;
use futures::future::{select, Either};
use hyper::body::{Body, Frame, SizeHint};
use hyper::header::{HeaderMap, HeaderName, HeaderValue};
use hyper::Uri;
use tokio::time::{sleep, sleep_until, Instant, Sleep};
use tonic::Status;
use tracing::error;
const GRPC_TIMEOUT: HeaderName = HeaderName::from_static("grpc-timeout");
const MAX_GRPC_TIMEOUT_VALUE: u128 = 99_999_999;
const GRPC_TIMEOUT_UNITS: [(char, u128); 6] = [
('n', 1),
('u', 1_000),
('m', 1_000_000),
('S', 1_000_000_000),
('M', 60 * 1_000_000_000),
('H', 60 * 60 * 1_000_000_000),
];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RequestTimeouts {
total: Option<Duration>,
idle: Option<Duration>,
}
impl RequestTimeouts {
pub const fn new() -> Self {
Self {
total: None,
idle: None,
}
}
pub const fn with_total(self, total: Duration) -> Self {
Self {
total: Some(total),
..self
}
}
pub const fn with_idle(self, idle: Duration) -> Self {
Self {
idle: Some(idle),
..self
}
}
pub(crate) fn start(self) -> Option<CallClock> {
if self.total.is_none() && self.idle.is_none() {
return None;
}
let started = Instant::now();
Some(CallClock {
started,
total: self
.total
.and_then(|limit| Some((limit, started.checked_add(limit)?))),
idle: self.idle,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RequestTimeoutElapsed {
Total(Duration),
Idle(Duration),
}
impl fmt::Display for RequestTimeoutElapsed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Total(limit) => write!(f, "total request timeout of {limit:?} elapsed"),
Self::Idle(limit) => write!(f, "idle request timeout of {limit:?} elapsed"),
}
}
}
impl std::error::Error for RequestTimeoutElapsed {}
impl From<RequestTimeoutElapsed> for Status {
fn from(elapsed: RequestTimeoutElapsed) -> Self {
let mut status = Status::deadline_exceeded(elapsed.to_string());
status.set_source(Arc::new(elapsed));
status
}
}
#[derive(Clone, Copy)]
pub(crate) struct CallClock {
started: Instant,
total: Option<(Duration, Instant)>,
idle: Option<Duration>,
}
impl CallClock {
pub(crate) fn insert_grpc_timeout(&self, headers: &mut HeaderMap) -> Result<(), Status> {
let Some((limit, deadline)) = self.total else {
return Ok(());
};
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(RequestTimeoutElapsed::Total(limit).into());
}
if let Some(value) = grpc_timeout_value(remaining) {
headers.entry(GRPC_TIMEOUT).or_insert(value);
}
Ok(())
}
pub(crate) async fn limit<B: 'static, E: From<Status>>(
self,
uri: Uri,
send: impl Future<Output = Result<hyper::Response<B>, E>>,
) -> Result<hyper::Response<B>, E> {
let total = self
.total
.map(|(limit, deadline)| (deadline, RequestTimeoutElapsed::Total(limit)));
let idle = self.idle.and_then(|limit| {
Some((
self.started.checked_add(limit)?,
RequestTimeoutElapsed::Idle(limit),
))
});
let response = match total.into_iter().chain(idle).min_by_key(|(at, _)| *at) {
Some((deadline, elapsed)) => {
match select(pin!(sleep_until(deadline)), pin!(send)).await {
Either::Left(_) => {
error!(
req_uri = %uri,
"Err: took {}ms ({elapsed})",
self.started.elapsed().as_millis()
);
return Err(Status::from(elapsed).into());
}
Either::Right((response, _)) => response?,
}
}
None => send.await?,
};
Ok(response.map(|body| self.limit_body(body)))
}
fn limit_body<B: 'static>(self, mut body: B) -> B {
if let Some(body) = (&mut body as &mut dyn Any).downcast_mut::<tonic::body::Body>() {
*body = tonic::body::Body::new(TimeoutBody {
inner: std::mem::take(body),
total: self
.total
.map(|(limit, deadline)| (limit, Box::pin(sleep_until(deadline)))),
idle: self.idle.map(|limit| IdleTimer {
limit,
sleep: Box::pin(sleep(limit)),
waiting: false,
}),
});
}
body
}
}
fn grpc_timeout_value(timeout: Duration) -> Option<HeaderValue> {
let nanos = timeout.as_nanos();
let (value, unit) = GRPC_TIMEOUT_UNITS
.iter()
.map(|&(unit, unit_nanos)| (nanos.div_ceil(unit_nanos), unit))
.find(|&(value, _)| value <= MAX_GRPC_TIMEOUT_VALUE)?;
HeaderValue::try_from(format!("{value}{unit}")).ok()
}
struct TimeoutBody {
inner: tonic::body::Body,
total: Option<(Duration, Pin<Box<Sleep>>)>,
idle: Option<IdleTimer>,
}
struct IdleTimer {
limit: Duration,
sleep: Pin<Box<Sleep>>,
waiting: bool,
}
impl Body for TimeoutBody {
type Data = Bytes;
type Error = Status;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
let this = self.get_mut();
if let Some((limit, deadline)) = &mut this.total {
if deadline.as_mut().poll(cx).is_ready() {
return Poll::Ready(Some(Err(RequestTimeoutElapsed::Total(*limit).into())));
}
}
if let Some(idle) = &mut this.idle {
if !idle.waiting {
if let Some(deadline) = Instant::now().checked_add(idle.limit) {
idle.sleep.as_mut().reset(deadline);
}
idle.waiting = true;
}
}
match Pin::new(&mut this.inner).poll_frame(cx) {
Poll::Ready(None) => {
this.total = None;
this.idle = None;
Poll::Ready(None)
}
Poll::Ready(frame) => {
if let Some(idle) = &mut this.idle {
idle.waiting = false;
}
Poll::Ready(frame)
}
Poll::Pending => {
if let Some(idle) = &mut this.idle {
if idle.sleep.as_mut().poll(cx).is_ready() {
return Poll::Ready(Some(Err(
RequestTimeoutElapsed::Idle(idle.limit).into()
)));
}
}
Poll::Pending
}
}
}
fn is_end_stream(&self) -> bool {
self.inner.is_end_stream()
}
fn size_hint(&self) -> SizeHint {
self.inner.size_hint()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::middleware::GoogleAuthMiddlewareService;
use crate::test_support::{silent_endpoint, stalling_stream_channel, StubCredentials};
use crate::{GoogleApi, GoogleAuthHeaders, GoogleAuthMiddleware};
use google_cloud_auth::credentials::Credentials;
use hyper::http::uri::PathAndQuery;
use std::error::Error as _;
use std::future::poll_fn;
use tokio::time::Instant;
use tonic::client::Grpc;
use tonic::codec::Streaming;
use tonic::Code;
use tonic_prost::ProstCodec;
const LIMIT: Duration = Duration::from_secs(5);
async fn unary(
service: GoogleAuthMiddleware,
request: tonic::Request<()>,
) -> Result<tonic::Response<()>, Status> {
let mut grpc = Grpc::new(service);
grpc.ready().await.unwrap();
grpc.unary(
request,
PathAndQuery::from_static("/test.Silent/Call"),
ProstCodec::default(),
)
.await
}
async fn open_stream(
service: GoogleAuthMiddleware,
request: tonic::Request<()>,
) -> Streaming<()> {
let mut grpc = Grpc::new(service);
grpc.ready().await.unwrap();
grpc.server_streaming(
request,
PathAndQuery::from_static("/test.StallingStream/Stream"),
ProstCodec::default(),
)
.await
.unwrap()
.into_inner()
}
#[tokio::test(start_paused = true)]
async fn total_limit_fails_a_call_the_server_never_answers() {
let (url, _) = silent_endpoint().await;
let client: GoogleApi<GoogleAuthMiddleware> = GoogleApi::from_function_with_credentials(
|service| service,
url,
None,
Credentials::from(StubCredentials::bearer("token")),
)
.await
.unwrap()
.with_request_timeouts(RequestTimeouts::new().with_total(LIMIT));
let started = Instant::now();
let status = unary(client.get(), tonic::Request::new(()))
.await
.unwrap_err();
assert_eq!(started.elapsed(), LIMIT);
assert_eq!(status.code(), Code::DeadlineExceeded);
assert_eq!(
status.source().and_then(|source| source.downcast_ref()),
Some(&RequestTimeoutElapsed::Total(LIMIT))
);
}
#[tokio::test(start_paused = true)]
async fn total_limit_past_milliseconds_fails_with_deadline_exceeded() {
let limit = Duration::from_micros(110_000_500);
let (url, _) = silent_endpoint().await;
let client: GoogleApi<GoogleAuthMiddleware> = GoogleApi::from_function_with_credentials(
|service| service,
url,
None,
Credentials::from(StubCredentials::bearer("token")),
)
.await
.unwrap()
.with_request_timeouts(RequestTimeouts::new().with_total(limit));
let started = Instant::now();
let status = unary(client.get(), tonic::Request::new(()))
.await
.unwrap_err();
assert!((limit..limit + Duration::from_millis(1)).contains(&started.elapsed()));
assert_eq!(status.code(), Code::DeadlineExceeded);
assert_eq!(
status.source().and_then(|source| source.downcast_ref()),
Some(&RequestTimeoutElapsed::Total(limit))
);
}
#[tokio::test(start_paused = true)]
async fn total_limit_ends_a_stream_that_keeps_delivering() {
let service = GoogleAuthMiddlewareService::new(
stalling_stream_channel(vec![Duration::from_secs(2); 10]).await,
GoogleAuthHeaders::from(Credentials::from(StubCredentials::bearer("token"))),
None,
)
.unwrap();
let mut request = tonic::Request::new(());
request
.extensions_mut()
.insert(RequestTimeouts::new().with_total(LIMIT));
let started = Instant::now();
let mut messages = open_stream(service, request).await;
let status = tokio::time::timeout(LIMIT * 2, async {
while messages.message().await?.is_some() {}
Ok::<_, Status>(())
})
.await
.expect("the total limit ends the stream")
.unwrap_err();
assert_eq!(started.elapsed(), LIMIT);
assert_eq!(status.code(), Code::DeadlineExceeded);
assert_eq!(
status.source().and_then(|source| source.downcast_ref()),
Some(&RequestTimeoutElapsed::Total(LIMIT))
);
}
#[tokio::test(start_paused = true)]
async fn limits_end_with_the_response_body() {
let clock = RequestTimeouts::new()
.with_total(LIMIT)
.with_idle(LIMIT)
.start()
.unwrap();
let mut body = clock.limit_body(tonic::body::Body::new(String::from("message")));
assert!(poll_fn(|cx| Pin::new(&mut body).poll_frame(cx))
.await
.is_some_and(|frame| frame.is_ok()));
assert!(poll_fn(|cx| Pin::new(&mut body).poll_frame(cx))
.await
.is_none());
tokio::time::sleep(LIMIT * 2).await;
assert!(poll_fn(|cx| Pin::new(&mut body).poll_frame(cx))
.await
.is_none());
}
#[tokio::test(start_paused = true)]
async fn idle_limit_fails_a_stream_that_stops_after_its_first_message() {
let service = GoogleAuthMiddlewareService::new(
stalling_stream_channel(vec![Duration::ZERO]).await,
GoogleAuthHeaders::from(Credentials::from(StubCredentials::bearer("token"))),
None,
)
.unwrap();
let mut request = tonic::Request::new(());
request
.extensions_mut()
.insert(RequestTimeouts::new().with_idle(LIMIT));
let mut messages = open_stream(service, request).await;
assert_eq!(messages.message().await.unwrap(), Some(()));
let started = Instant::now();
let status = tokio::time::timeout(LIMIT * 2, messages.message())
.await
.expect("the idle limit ends the wait")
.unwrap_err();
assert_eq!(started.elapsed(), LIMIT);
assert_eq!(status.code(), Code::DeadlineExceeded);
assert_eq!(
status.source().and_then(|source| source.downcast_ref()),
Some(&RequestTimeoutElapsed::Idle(LIMIT))
);
}
#[tokio::test(start_paused = true)]
async fn idle_limit_spares_a_slow_stream_and_a_slow_reader() {
let gaps = [3, 3, 7].map(Duration::from_secs).to_vec();
let mut service = GoogleAuthMiddlewareService::new(
stalling_stream_channel(gaps).await,
GoogleAuthHeaders::from(Credentials::from(StubCredentials::bearer("token"))),
None,
)
.unwrap();
service.set_request_timeouts(RequestTimeouts::new().with_total(LIMIT));
let mut request = tonic::Request::new(());
request
.extensions_mut()
.insert(RequestTimeouts::new().with_idle(LIMIT));
let mut messages = open_stream(service, request).await;
assert_eq!(messages.message().await.unwrap(), Some(()));
assert_eq!(messages.message().await.unwrap(), Some(()));
tokio::time::sleep(Duration::from_secs(4)).await;
assert_eq!(messages.message().await.unwrap(), Some(()));
}
#[tokio::test(start_paused = true)]
async fn without_limits_a_call_waits_for_the_connection() {
let (url, _) = silent_endpoint().await;
let client: GoogleApi<GoogleAuthMiddleware> = GoogleApi::from_function_with_credentials(
|service| service,
url,
None,
Credentials::from(StubCredentials::bearer("token")),
)
.await
.unwrap();
let call = unary(client.get(), tonic::Request::new(()));
assert!(tokio::time::timeout(Duration::from_secs(60), call)
.await
.is_err());
}
}