-
Notifications
You must be signed in to change notification settings - Fork 1.2k
refactor(grpc): Use gRPC metadata instead of tonic metadata #2629
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
Changes from 6 commits
6f3f2eb
6c87f8b
25812ea
2af18ab
f90a584
751f4e1
09c4e35
34368ff
3c214c0
0d95d02
e0a88d6
8b86c43
486692c
f6873ec
bcd7655
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,7 @@ use tonic::codec::Codec; | |
| use tonic::codec::Decoder; | ||
| use tonic::codec::EncodeBuf; | ||
| use tonic::codec::Encoder; | ||
| use tonic::metadata::MetadataMap; | ||
| use tonic::metadata::MetadataMap as TonicMeta; | ||
| use tower::ServiceBuilder; | ||
| use tower::buffer::Buffer; | ||
| use tower::buffer::future::ResponseFuture as BufferResponseFuture; | ||
|
|
@@ -154,7 +154,7 @@ impl Invoke for TonicTransport { | |
| let request_stream = ReceiverStream::new(req_rx); | ||
| let mut request = TonicRequest::new(Box::pin(request_stream)); | ||
| let (method, metadata) = headers.into_parts(); | ||
| *request.metadata_mut() = metadata; | ||
| *request.metadata_mut() = metadata.into(); | ||
|
|
||
| let Ok(path) = PathAndQuery::from_maybe_shared(method) else { | ||
| return err_streams(StatusError::new(StatusCodeError::Internal, "invalid path")); | ||
|
|
@@ -192,28 +192,38 @@ impl Invoke for TonicTransport { | |
| // Converts from a tonic status to a trailers stream item. | ||
| fn trailers_from_tonic_status( | ||
| status: TonicStatus, | ||
| md: Option<MetadataMap>, | ||
| md: Option<TonicMeta>, | ||
| ) -> ClientResponseStreamItem { | ||
| let mut trailers = Trailers::new(if status.code() == Code::Ok { | ||
| Ok(()) | ||
| } else { | ||
| Err(StatusError::new( | ||
| StatusCodeError::from(status.code() as i32), | ||
| let status_res = match status.code() { | ||
| Code::Ok => Ok(()), | ||
| code => Err(StatusError::new( | ||
| StatusCodeError::from(code as i32), | ||
| status.message(), | ||
| )) | ||
| }); | ||
| if let Some(md) = md { | ||
| trailers = trailers.with_metadata(md); | ||
| } | ||
| )), | ||
| }; | ||
|
|
||
| let trailers = match md.map(TryInto::try_into) { | ||
| Some(Err(e)) => Trailers::new(Err(StatusError::new( | ||
| StatusCodeError::Internal, | ||
| format!("failed to parse metadata: {e}"), | ||
| ))), | ||
| Some(Ok(metadata)) => Trailers::new(status_res).with_metadata(metadata), | ||
| None => Trailers::new(status_res), | ||
| }; | ||
|
|
||
| ClientResponseStreamItem::Trailers(trailers) | ||
| } | ||
|
|
||
| // Builds a trailers with a status | ||
| fn trailers_from_status(status: Status, md: Option<MetadataMap>) -> ClientResponseStreamItem { | ||
| let mut trailers = Trailers::new(status); | ||
| if let Some(md) = md { | ||
| trailers = trailers.with_metadata(md); | ||
| } | ||
| fn trailers_from_status(status: Status, md: Option<TonicMeta>) -> ClientResponseStreamItem { | ||
| let trailers = match md.map(TryInto::try_into) { | ||
| Some(Err(e)) => Trailers::new(Err(StatusError::new( | ||
| StatusCodeError::Internal, | ||
| format!("failed to parse metadata: {e}"), | ||
| ))), | ||
| Some(Ok(metadata)) => Trailers::new(status).with_metadata(metadata), | ||
| None => Trailers::new(status), | ||
| }; | ||
| ClientResponseStreamItem::Trailers(trailers) | ||
| } | ||
|
|
||
|
|
@@ -262,11 +272,33 @@ impl RecvStream for TonicRecvStream { | |
| StreamState::AwaitingHeaders(rx) => match rx.await { | ||
| Ok(Ok(response)) => { | ||
| let (metadata, stream, _extensions) = response.into_parts(); | ||
| // Start streaming and return the headers. | ||
| self.state = StreamState::Streaming(stream); | ||
| ClientResponseStreamItem::Headers( | ||
| ResponseHeaders::new().with_metadata(metadata), | ||
| ) | ||
| // Tonic decodes base64-encoded binary headers lazily. It | ||
| // does not fail the RPC upon receiving invalid base64 data; | ||
| // the error only surfaces when the application attempts to | ||
| // read the metadata. | ||
| // In contrast, standard gRPC implementations eagerly decode | ||
| // these headers and immediately fail the RPC with an | ||
| // Internal status. | ||
| match metadata.try_into() { | ||
| Ok(md) => { | ||
| // Start streaming and return the headers. | ||
| self.state = StreamState::Streaming(stream); | ||
| ClientResponseStreamItem::Headers( | ||
| ResponseHeaders::new().with_metadata(md), | ||
| ) | ||
| } | ||
| // TODO: in this case, tonic believes the stream is | ||
| // still running, but our parsing failed -- do we need | ||
| // to terminate the request stream now even though the | ||
| // Streaming is dropped? | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's answer this before merging this PR if we can.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially assumed that either Tonic, Hyper, or h2 would automatically close the underlying HTTP/2 stream when the client drops the inbound stream. However, this is not the case. I verified this by modifying the Modified route_chat clientasync fn run_route_chat(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
let start = time::Instant::now();
let outbound = async_stream::stream! {
let mut interval = time::interval(Duration::from_secs(1));
loop {
let time = interval.tick().await;
let elapsed = time.duration_since(start);
let note = RouteNote {
location: Some(Point {
latitude: 409146138 + elapsed.as_secs() as i32,
longitude: -746188906,
}),
message: format!("at {elapsed:?}"),
};
println!("Sending message: {:?}", note.clone());
yield note;
}
};
let response = client.route_chat(Request::new(outbound)).await?;
let mut inbound = response.into_inner();
// Drop the inbound stream immediately
drop(inbound);
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
Ok(())
}Despite dropping I updated the Tonic transport to explicitly close the outbound stream when it fails to decode response messages or headers on the inbound stream. With this change, the Go server receives a RST_STREAM frame with code To implement this without introducing a mutex or spawning an additional background task, I used the take_until stream combinator from the Update: Also ensured dropping |
||
| Err(e) => trailers_from_status( | ||
| Err(StatusError::new( | ||
| StatusCodeError::Internal, | ||
| format!("error decoding response: {e}"), | ||
| )), | ||
| None, | ||
| ), | ||
| } | ||
| } | ||
| // Stay closed after sending trailers. | ||
| Err(_) => trailers_from_status( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this just call
trailers_from_statusto do all of this part?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that removed the redundant error handling. Updated.