-
Notifications
You must be signed in to change notification settings - Fork 4.5k
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
server: return better status for context err when writing header #5292
server: return better status for context err when writing header #5292
Conversation
|
4811f7a
to
3872708
Compare
3872708
to
1715f7e
Compare
87e4962
to
6f33edb
Compare
internal/transport/http2_server.go
Outdated
@@ -53,10 +52,10 @@ import ( | |||
var ( | |||
// ErrIllegalHeaderWrite indicates that setting header is illegal because of | |||
// the stream's state. | |||
ErrIllegalHeaderWrite = errors.New("transport: the stream is done or WriteHeader was already called") | |||
ErrIllegalHeaderWrite = status.Error(codes.Internal, "transport: the stream is done or WriteHeader was already called") |
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.
Please update this text since we're more precise about sending this error now. E.g. "stream.SendHeader called multiple times"
. (It's SendHeader
at the user API level.)
@@ -946,7 +950,7 @@ func (t *http2Server) WriteHeader(s *Stream, md metadata.MD) error { | |||
} | |||
if err := t.writeHeaderLocked(s); err != nil { |
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.
GH won't let me comment on the right line, but on line 981, there's no guarantee this is a status error. I think it's always ErrConnClosing
, in fact. Let's make that return status.Convert(err).Err()
-- it's not really efficient or ideal, but it's only in an error condition so it should be okay. This means a closed connection will result in UNKNOWN, but since I'm pretty sure using statuses at all here is a bad idea in the first place, that should be fine. I think I'll separately change all the documentation on ServerStream
to recommend against relying upon the returned errors' status codes.
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.
Are you suggesting changing the error inside writeHeaderLocked
to be status.Convert(err).Err()
or changing the handling of the return value to be status.Convert(err).Err()
?
I changed the outside error, as that's the place where we converted it to status.Internal
previously. Let me know if I understood wrong.
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.
Either way is probably fine. There is only one other usage of writeHeaderLocked
, in WriteStatus
, and the ultimate user of that just logs the result.
internal/transport/http2_server.go
Outdated
@@ -933,9 +932,14 @@ func (t *http2Server) checkForHeaderListSize(it interface{}) bool { | |||
|
|||
// WriteHeader sends the header metadata md back to the client. | |||
func (t *http2Server) WriteHeader(s *Stream, md metadata.MD) error { | |||
if s.updateHeaderSent() || s.getState() == streamDone { | |||
if s.getState() == streamDone { | |||
return ContextErr(s.ctx.Err()) |
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.
There's a small window where the state can be streamDone
but the context hasn't been canceled yet. In these cases, we would be returning an INTERNAL error.
We could paper over this race by calling s.cancel()
here, as we seem to do in Write
. But honestly I think the right fix here is to move s.cancel()
in deleteStream
to the first line of both finishStream
and cancelStream
. And then we can remove the s.cancel()
in Write
. Would you mind doing this and hopefully (:crossed_fingers:) all the tests still pass?
Looking closer at Write
, it looks like it has special logic to prefer returning an error that the whole connection was closed over the stream being closed. We probably want to do the same thing here. If you want to copy/paste the select, that's fine. Or if you don't mind refactoring, it looks like we need:
func (t *http2Server) streamContextErr(s *Stream) error {
select {
case <-t.done:
return ErrConnClosing
default:
}
return ContextErr(s.ctx.Err())
}
But I can do that as a follow-on later otherwise.
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.
Yup, tests seem to all pass with this! I changed it finishStream
, but it looks like it's already called in deleteStream
(which closeStream
calls).
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.
It is done by deleteStream
, but there is a small race, still, where the context is not canceled while the state is streamDone
. So I was suggesting moving that cancelation to happen before setting the stream's state to streamDone
, and also removing it from deleteStream
.
Also is it possible to run the tests here? I'm a first time contributor so I think running the CI workflow requires signoff. |
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.
Thanks! I think everything looks good except for moving the s.cancel
call to closeStream
directly, and out of deleteStream
.
@@ -946,7 +950,7 @@ func (t *http2Server) WriteHeader(s *Stream, md metadata.MD) error { | |||
} | |||
if err := t.writeHeaderLocked(s); err != nil { |
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.
Either way is probably fine. There is only one other usage of writeHeaderLocked
, in WriteStatus
, and the ultimate user of that just logs the result.
internal/transport/http2_server.go
Outdated
@@ -933,9 +932,14 @@ func (t *http2Server) checkForHeaderListSize(it interface{}) bool { | |||
|
|||
// WriteHeader sends the header metadata md back to the client. | |||
func (t *http2Server) WriteHeader(s *Stream, md metadata.MD) error { | |||
if s.updateHeaderSent() || s.getState() == streamDone { | |||
if s.getState() == streamDone { | |||
return ContextErr(s.ctx.Err()) |
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.
It is done by deleteStream
, but there is a small race, still, where the context is not canceled while the state is streamDone
. So I was suggesting moving that cancelation to happen before setting the stream's state to streamDone
, and also removing it from deleteStream
.
internal/transport/http2_server.go
Outdated
} | ||
// TODO(mmukhi, dfawley): Make sure this is the right code to return. | ||
return status.Errorf(codes.Internal, "transport: %v", err) | ||
return err | ||
} | ||
} else { | ||
// Writing headers checks for this condition. | ||
if s.getState() == streamDone { | ||
// TODO(mmukhi, dfawley): Should the server write also return io.EOF? |
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.
I think this can be deleted now.
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.
The TODO or the entire conditional? The tests appear to pass with the entire conditional removed but I'm not familiar enough with this code to understand whether we still need to check if the stream is done elsewhere in Write
(if it's called after writing headers).
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.
It looks like writeQueue.get
checks whether the writeQueue
is done
. Is the idea that that channel is set when the stream is closed?
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.
I was just referring to the TODO. I don't think this whole thing should be removed or else we'll potentially write operations into the control buffer for closed streams. If the controlbuf isn't checking that the stream is still valid and writes frames, then that's an HTTP/2 protocol violation.
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.
ack. reverted and removed the TODO
.
134a4b8
to
3399614
Compare
3399614
to
32d8958
Compare
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.
LGTM
I think this closes #4696
It looks like
Write
unconditionally returnsstatus.Errorf(codes.Internal, "transport: %v", err)
when we see an error writing the headers. However, in certain cases we'll actually have an overriding context error here, like a cancellation. This PR changes the behavior to return that instead if it's present.This matches the behavior in the other branch of the conditional (
grpc-go/internal/transport/http2_server.go
Line 1028 in 1f12bf4
streamDone
, this either returnsErrConnClosing
or thes.ctx.Err()
. We already check for the stream beingstreamDone
insideWriteHeader
, so I opted not to duplicate that check and instead just return the ctx error if it's present.If we wanted a more specific fix, this could change
to
but it duplicates the check inside
WriteHeader
.(This is my first gRPC contribution, so apologies if I haven't followed the right process or I've overlooked something)
RELEASE NOTES: