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

transport: ensure client always closes streams #2354

Merged
merged 2 commits into from
Oct 15, 2018
Merged

Conversation

siggy
Copy link
Contributor

@siggy siggy commented Oct 6, 2018

When http2Client receives END_STREAM from the server, it calls
closeStream, but does not send END_STREAM back to the server. This
leaves the stream in a half-closed state.

This change ensures that END_STREAM is sent back to the server,
putting the stream in a fully closed state. This motivated modifying the
streamState type to more closely model the state transitions defined
in https://http2.github.io/http2-spec/#StreamStates. Specifically:

  • active -> [recv ES] -> readDone
  • active -> [send ES] -> writeDone
  • readDone -> [send ES] -> done
  • writeDone -> [recv ES] -> done

Reproduction

PCAP files, one before the change, one after:
grpc-end-stream-pcap.zip

Existing behavior (without END_STREAM)

Get test repo

go get github.com/buoyantio/strest-grpc
cd $GOPATH/src/github.com/buoyantio/strest-grpc

Start server

go run main.go ref-server
INFO[0000] starting gRPC server on :11111

Run client

(rpc error is expected here, just needed a way to reproduce a server sending an END_STREAM)

go run main.go ref-client
INFO[0000] connecting to localhost:11111
ERRO[0000] stream.Recv() returned unexpected error: rpc error: code = Unknown desc = invalid ResponseSpec, Count cannot be negative

grpc-end-stream before

New behavior (with END_STREAM)

Switch to modified branch (includes this PR's change in vendor)

git checkout siggy/grpc-eos-test

Start server

go run main.go ref-server
INFO[0000] starting gRPC server on :11111

Run client

go run main.go ref-client
INFO[0000] connecting to localhost:11111
ERRO[0000] stream.Recv() returned unexpected error: rpc error: code = Unknown desc = invalid ResponseSpec, Count cannot be negative

grpc-end-stream after

When `http2Client` receives `END_STREAM` from the server, it calls
`closeStream`, but does not send `END_STREAM` back to the server. This
leaves the stream in a half-closed state.

This change ensures that `END_STREAM` is sent back to the server,
putting the stream in a fully closed state. This motivated modifying the
`streamState` type to more closely model the state transitions defined
in https://http2.github.io/http2-spec/#StreamStates. Specifically:
- active -> [recv ES] -> readDone
- active -> [send ES] -> writeDone
- readDone -> [send ES] -> done
- writeDone -> [recv ES] -> done
@@ -680,8 +680,13 @@ func (t *http2Client) CloseStream(s *Stream, err error) {
}

func (t *http2Client) closeStream(s *Stream, err error, rst bool, rstCode http2.ErrCode, st *status.Status, mdata map[string][]string, eosReceived bool) {
if !s.isStreamWriteDone() {
// send END_STREAM
t.Write(s, nil, nil, &Options{Last: true})
Copy link
Contributor Author

@siggy siggy Oct 6, 2018

Choose a reason for hiding this comment

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

Just calling out this is where the behavior change is. I want to confirm that it's safe to attempt a http2Client.Write() here, particularly because our call stack originates with a read from the server (where it receives an END_STREAM):

  • http2Client.Write()
  • http2Client.closeStream()
  • http2Client.operateHeaders()
  • http2Client.reader()

From debug.Stack():

github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport.(*http2Client).closeStream(0xc00009ed00, 0xc0001d6000, 0x1765980, 0xc000090050, 0x1e8000, 0xc0001ea010, 0xc0001e8120, 0x1)
	/Users/sig/code/go/src/github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport/http2_client.go:669 +0x302
github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport.(*http2Client).operateHeaders(0xc00009ed00, 0xc0001e8090)
	/Users/sig/code/go/src/github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport/http2_client.go:1156 +0x2f4
github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport.(*http2Client).reader(0xc00009ed00)
	/Users/sig/code/go/src/github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport/http2_client.go:1213 +0x70f
created by github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport.newHTTP2Client
	/Users/sig/code/go/src/github.com/buoyantio/strest-grpc/vendor/google.golang.org/grpc/internal/transport/http2_client.go:263 +0xaf8

Copy link
Contributor

Choose a reason for hiding this comment

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

This should work, but let's do a RST_STREAM instead.

The ideal process of a normal RPC is, the client sends end-of-stream when it's done sending, and the server finishes the RPC with trailer (and also is end-of-stream).
So when the client receives an end-of-stream before it sends end-of-stream, it is unexpected. So RST_STREAM sounds better here.

closeStream is already sending RST_STREAM to terminate the stream when necessary. The parameters rst and rstCode are for that. So if you set rst to true, things should happen automatically. We can use http2.ErrCodeNo for the error code.

Copy link
Contributor

@menghanl menghanl left a comment

Choose a reason for hiding this comment

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

Thanks for the fix!

I have some comments on END_STREAM vs RST_STREAM. Let me know what you think. Thanks!

@@ -195,7 +195,9 @@ type Stream struct {
// On the server-side, headerSent is atomically set to 1 when the headers are sent out.
headerSent uint32

state streamState
// stateMu protects stream state transitions
stateMu sync.Mutex
Copy link
Contributor

Choose a reason for hiding this comment

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

We tried to avoid this mutex here for performance reasons, and that's why we do those atomic actions.

The state transition in gRPC is limited comparing with the http2 spec:

  • the client will never be in read-done mode because that means the end of the stream, so the transition can only be active -> [writeDone] -> done
  • the server will never be in write-done mode, and it can only be active -> [readDone] -> done

However, because the Stream struct is shared by the client and the server, so the state is made a atomic, and the logic is handled in the client/server specific code. We have plans to split Stream for client and server, and will be able to better solve this problem.

I would suggest let's revert this back to the previous state if you are OK with it.

@@ -680,8 +680,13 @@ func (t *http2Client) CloseStream(s *Stream, err error) {
}

func (t *http2Client) closeStream(s *Stream, err error, rst bool, rstCode http2.ErrCode, st *status.Status, mdata map[string][]string, eosReceived bool) {
if !s.isStreamWriteDone() {
// send END_STREAM
t.Write(s, nil, nil, &Options{Last: true})
Copy link
Contributor

Choose a reason for hiding this comment

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

This should work, but let's do a RST_STREAM instead.

The ideal process of a normal RPC is, the client sends end-of-stream when it's done sending, and the server finishes the RPC with trailer (and also is end-of-stream).
So when the client receives an end-of-stream before it sends end-of-stream, it is unexpected. So RST_STREAM sounds better here.

closeStream is already sending RST_STREAM to terminate the stream when necessary. The parameters rst and rstCode are for that. So if you set rst to true, things should happen automatically. We can use http2.ErrCodeNo for the error code.

based on review feedback

Signed-off-by: Andrew Seigner <[email protected]>
siggy added a commit to BuoyantIO/strest-grpc that referenced this pull request Oct 9, 2018
changes based on grpc/grpc-go#2354

Signed-off-by: Andrew Seigner <[email protected]>
@siggy
Copy link
Contributor Author

siggy commented Oct 9, 2018

Thanks for the thorough review @menghanl ! I've made your suggested changes, much simpler.

Confirmed RST_STREAM is now sent from the client:
grpc-rst-stream
grpc-rst-stream.pcapng.zip

@siggy siggy changed the title internal: ensure http2Client sends END_STREAM internal: ensure http2Client sends RST_STREAM Oct 9, 2018
siggy added a commit to linkerd/linkerd that referenced this pull request Oct 10, 2018
`maxConcurrentStreamsPerConnection`'s default value was unlimited,
meaning Linkerd could run out of memory if a client created enough
streams, particularly if the client is leaking streams but not properly
closing them.

Change default `maxConcurrentStreamsPerConnection` value to 1000,
disallow unlimited via a Linkerd config file.

Related to #2132 and grpc/grpc-go#2354

Signed-off-by: Andrew Seigner <[email protected]>
siggy added a commit to linkerd/linkerd that referenced this pull request Oct 10, 2018
`maxConcurrentStreamsPerConnection`'s default value was unlimited,
meaning Linkerd could run out of memory if a client created enough
streams, particularly if the client is leaking streams but not properly
closing them.

Change default `maxConcurrentStreamsPerConnection` value to 1000,
disallow unlimited via a Linkerd config file.

Related to #2132 and grpc/grpc-go#2354

Signed-off-by: Andrew Seigner <[email protected]>
Copy link
Contributor

@menghanl menghanl left a comment

Choose a reason for hiding this comment

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

Thanks for the fix! LGTM.

@menghanl menghanl changed the title internal: ensure http2Client sends RST_STREAM transport: ensure http2Client sends RST_STREAM Oct 15, 2018
@menghanl menghanl merged commit 1da8e51 into grpc:master Oct 15, 2018
@menghanl menghanl added this to the 1.16 Release milestone Oct 15, 2018
@menghanl menghanl changed the title transport: ensure http2Client sends RST_STREAM transport: ensure client always closes streams Oct 23, 2018
@lock lock bot locked as resolved and limited conversation to collaborators Apr 21, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants