-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Maybe stream should be broken with None after an error #222
Comments
So I think in this case you probably want to use |
Ah, neat, yeah Thanks for the pointer about message too and I'll see about using one or the other here. Yeah, I'm not entirely sure about the ideal way to handle stream termination here, but my gut feeling is that if Tonic knows that a stream has effectively been terminated (even if it's due to some error condition) then it should probably report None within a finite amount of time. If Tonic knows that an error is fatal with respect to a stream then I guess after reporting the error it could internally mark the stream as terminated and that status would have higher precedence than any persistent error condition, and None could be sent on the next read. Rust's stream polling seems to be designed to allow for streams to contain multiple errors and in general errors within a stream don't have to imply that the stream has ended. Earlier versions of Stream (https://docs.rs/futures/0.2.1/futures/stream/trait.Stream.html) which used to return a Result from poll_next were documented with this:
Now stream polling is even more orthogonal from error reporting, but the above idea still seems to makes sense with the final Stream API too. Considering that at the lowest level stream polling doesn't care about errors flowing through. then I think that gives me some expectation that None would be the most reliable, highest precedence, indicator of a stream ending and errors are per-stream implementation details. I would think about checking None before checking for errors because the former is a more fundamental state for the stream and errors are optional (in general for rust I mean - not in this specific case). Semantically it seems to me like Tonic is not terminating the stream for some stream-fatal error conditions, and it's instead transitioning the stream in to a perpetual error stream. Maybe that's fine (it's simple enough to handle if this is known) but maybe it goes against the design spirit of rust streams a bit, to require errors to be interpreted as stream terminators? Playing devils advocate really ;-p I haven't really looked at lots of apis to have a good sense of what the best choice is and I'm probably biased now anyway :) |
So I am going to remove this from the |
While you can use |
Once a stream hits an error it will report it once then return |
I think that fixes this issue when an outgoing stream of responses reports errors, but not when an incoming stream of requests reports errors. |
I'm not totally following, you mean a client sending messages on a client side stream -> server? There shouldn't be any errors in that case iirc. Also Monday so may not be thinking correctly just yet :D |
client.rs: pub mod echo {
tonic::include_proto!("grpc.examples.echo");
}
use echo::{echo_client::EchoClient, EchoRequest};
use futures::stream;
use tonic::transport::Endpoint;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let channel = Endpoint::from_static("http://[::1]:50051")
.connect()
.await?;
let mut echo_client = EchoClient::new(channel);
echo_client
.client_streaming_echo(stream::repeat(EchoRequest {
message: "hello".into(),
}))
.await;
Ok(())
} server.rs: use futures::{Stream, StreamExt};
use std::pin::Pin;
use tonic::{transport::Server, Request, Response, Status};
pub mod echo {
tonic::include_proto!("grpc.examples.echo");
}
use echo::{
echo_server::{Echo, EchoServer},
EchoRequest, EchoResponse,
};
type ResponseStream = Pin<Box<dyn Stream<Item = Result<EchoResponse, Status>> + Send + Sync>>;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let echo = EchoServer::new(MyEcho::default());
Server::builder().add_service(echo).serve(addr).await?;
Ok(())
}
#[derive(Default)]
pub struct MyEcho;
#[tonic::async_trait]
impl Echo for MyEcho {
async fn unary_echo(&self, _: Request<EchoRequest>) -> Result<Response<EchoResponse>, Status> {
Err(Status::unimplemented("Not yet implemented"))
}
type ServerStreamingEchoStream = ResponseStream;
async fn server_streaming_echo(
&self,
_: Request<EchoRequest>,
) -> Result<Response<Self::ServerStreamingEchoStream>, Status> {
Err(Status::unimplemented("Not yet implemented"))
}
async fn client_streaming_echo(
&self,
request: Request<tonic::Streaming<EchoRequest>>,
) -> Result<Response<EchoResponse>, Status> {
let mut stream = request.into_inner();
while let Some(r) = stream.next().await {
println!("{:#?}", r);
}
Ok(Response::new(EchoResponse {
message: "hi".to_string(),
}))
}
type BidirectionalStreamingEchoStream = ResponseStream;
async fn bidirectional_streaming_echo(
&self,
_: Request<tonic::Streaming<EchoRequest>>,
) -> Result<Response<Self::BidirectionalStreamingEchoStream>, Status> {
Err(Status::unimplemented("Not yet implemented"))
}
} Run both, then kill the client. The server will infinitely print
|
Btw I am still looking into this but was originally having trouble reproducing this in a test. So will continue to look into this. |
Ok here is an attempted fix that should in general just return None when we've seen an error from poll_data. So it should go |
Within a Tonic server, handling a request where a client opens a 'keep alive' stream, I got bitten today by only checking for
None
to determine if a client had disappeared and the stream was closed.I.e. I had a loop like this to read the stream:
but after closing the client, it ended up being considered an error within Tonic that actually left my server code stuck in an infinite loop after that client dissappeared, since I had neglected to check the inner
Result
status returned by.next().await
.Arguably it's just my bug that I ignored the result status, but also perhaps arguably the stream has ended, and maybe Tonic could recognise this and limit how many errors may be reported before marking the stream end with
None
?Just for reference, this is the error I saw earlier:
tonic::codec::decode: decoder inner stream error: Status { code: Unknown, message: "error reading a body from connection: protocol error: stream no longer needed" }
Though it wasn't entirely clear to me what the error really meant.
and the preceding
h2
tracing made it seem to me like a fairly clean break by the client:The fix for my server was simple enough, after realising the issue - but filing this in case it makes sense to tweak Tonic's error handling to ensure a disconnected client won't leave a stream in an endless error state.
The text was updated successfully, but these errors were encountered: