Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support timeouts with "grpc-timeout" header #516

Closed
wants to merge 3 commits into from

Conversation

ParkMyCar
Copy link

@ParkMyCar ParkMyCar commented Dec 29, 2020

Motivation

This PR fixes issue #75, which is to implement support for the "grpc-timeout" header in tonic. Setting a timeout/deadline is an official part of the gRPC spec, and is a recommended practice.

Solution

Server

This PR creates a new Timeout struct that implements tower::Service and is added as a layer_fn(...) in a Connection's tower::ServiceBuilder. The Timeout service then parses the grpc-timeout header, and wraps an inner future in a timeout.

I'm relying on tokio::time::Delay for the actual timeout logic (Delay seems to be 1:1 with Sleep in tokio 1.0, which should make migration easy). I realize this prevents the Timeout service from being runtime agnostic, but since the service lives within the transport module, which already depends on tokio, I thought this was okay. If we don't want to rely on tokio::time::Delay, we can hand write our own Delay-like future, with a downside being, we won't be able to use mock timers for testing.

A tonic::transport::Server already supports a default timeout, I handle the presence of a server timeout, and a grpc-timeout header, by using which ever duration is the shortest, essentially the server timeout becomes a ceiling. The reasoning here is I didn't want a client to be able to override a server's settings, but if a client wants to timeout faster, the server shouldn't care about that.

Client

This PR doesn't (yet) make any changes to gRPC clients. It could, similar to what the gRPC docs have as examples for other libraries, but I thought the server/client impls would be a good separation for different PRs.

Note

I had previously opened #362 a few months ago which tries to add the same feature, supporting timeouts with the "grpc-timeout" header. I'm opening a new PR now because I had deleted my previous fork of tonic that was used to open that previous PR. I hope this is okay 🙂

"te",
"user-agent",
"content-type",
"grpc-timeout",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing this definitely isn't right, but otherwise the header was getting stripped when I tried to set it in the integration test. Would love some guidance on how to set this header from a test

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its fine. As the user is now allowed to set it I guess its not reserved anymore.

/// should never happen in practice because the `grpc-timeout` header should be only
/// ASCII characters.
fn try_split_last(val: &str) -> Result<(&str, &str), &str> {
std::panic::catch_unwind(|| val.split_at(val.len() - 1)).map_err(|_| val)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized, after writing a unit test, that HeaderValue already prevents non-ASCII characters 🎉 so this extra catch_unwind(|| ...) probably isn't necessary, but will leave it in for now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess str.split_at(...) should be fine since HeaderValue::to_str checks for ASCII as you mention.

@Sushisource
Copy link
Contributor

@LucioFranco Anything I could do to move along at least the client-side support for setting the timeout header? I'm planning on releasing a crate soon that requires it, and it would be great to not need to [patch] tonic.

I'm guessing you want to do something a little more formal than just removing it from reserved headers. If you let me know what that might be I would be happy to make a PR.

Copy link
Member

@davidpdrsn davidpdrsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ParkMyCar Thanks for working on this! I've left a couple of comments but overall I think the approach looks good 👍

Are you still up for driving this home? Getting this up to date with master might require some work as tonic has updated to tokio 1.x.

I do think having to create a new middleware not being able to use tower::timeout::Timeout is unfortunate but I don't see a nice way around it that doesn't involve creating new middlewares regardless.

let err = client.unary_call(req).await.unwrap_err();
assert!(err.message().contains("Timeout expired"));
// TODO: Need to return the correct type of code
// assert_eq!(err.code(), Code::Cancelled);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like something that should be fixed before merging 🤔

"te",
"user-agent",
"content-type",
"grpc-timeout",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its fine. As the user is now allowed to set it I guess its not reserved anymore.

fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
// Try to parse the `grpc-timeout` header, if it is present
let header_timeout = headers::try_parse_grpc_timeout(req.headers()).unwrap_or_else(|e| {
warn!("Error parsing grpc-timeout header {:?}", e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not sure warn is the best fit here since clients might be sending garbage data. It isn't really tonics fault, modulo parsing bugs of course 😊 I kinda think we should use trace or drop the log entirely.

(None, Some(dur)) => Some(dur),
(Some(header), Some(server)) => {
let shorter_duration = std::cmp::min(header, server);
debug!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking this should just be trace.

Suggested change
debug!(
trace!(

/// should never happen in practice because the `grpc-timeout` header should be only
/// ASCII characters.
fn try_split_last(val: &str) -> Result<(&str, &str), &str> {
std::panic::catch_unwind(|| val.split_at(val.len() - 1)).map_err(|_| val)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess str.split_at(...) should be fine since HeaderValue::to_str checks for ASCII as you mention.

@davidpdrsn
Copy link
Member

I'm going to be driving this home in a separate PR.

@davidpdrsn
Copy link
Member

Superseded by #606

@davidpdrsn davidpdrsn closed this Apr 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants