streams: Stop cleaning up after orphaned streams (#1854)
This change introduces some behavior changes that should not impact users that
are following the proper stream protocol. Specifically, one of the following
conditions must be satisfied:
1. The user calls Close on the ClientConn.
2. The user cancels the context provided to NewClientStream, or its deadline
expires. (Note that it if the context is no longer needed before the deadline
expires, it is still recommended to call cancel to prevent bloat.) It is always
recommended to cancel contexts when they are no longer needed, and to
never use the background context directly, so all users should always be
doing this.
3. The user calls RecvMsg (or Recv in generated code) until a non-nil error is
returned.
4. The user receives any error from Header or SendMsg (or Send in generated
code) besides io.EOF. If none of the above happen, this will leak a goroutine
and a context, and grpc will not call the optionally-configured stats handler
with a stats.End message.
Before this change, if a user created a stream and the server ended the stream,
the stats handler would be invoked with a stats.End containing the final status
of the stream. Subsequent calls to RecvMsg would then trigger the stats handler
with InPayloads, which may be unexpected by stats handlers.
This commit is contained in:
176
stream.go
176
stream.go
@ -114,34 +114,28 @@ func NewClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
|
||||
}
|
||||
|
||||
func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) {
|
||||
var (
|
||||
t transport.ClientTransport
|
||||
s *transport.Stream
|
||||
done func(balancer.DoneInfo)
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
c := defaultCallInfo()
|
||||
mc := cc.GetMethodConfig(method)
|
||||
if mc.WaitForReady != nil {
|
||||
c.failFast = !*mc.WaitForReady
|
||||
}
|
||||
|
||||
// Possible context leak:
|
||||
// The cancel function for the child context we create will only be called
|
||||
// when RecvMsg returns a non-nil error, if the ClientConn is closed, or if
|
||||
// an error is generated by SendMsg.
|
||||
// https://github.com/grpc/grpc-go/issues/1818.
|
||||
var cancel context.CancelFunc
|
||||
if mc.Timeout != nil && *mc.Timeout >= 0 {
|
||||
// The cancel function for this context will only be called when RecvMsg
|
||||
// returns non-nil error, which means the stream finishes with error or
|
||||
// io.EOF. https://github.com/grpc/grpc-go/issues/1818.
|
||||
//
|
||||
// Possible context leak:
|
||||
// - If user provided context is Background, and the user doesn't call
|
||||
// RecvMsg() for the final status, this ctx will be leaked after the
|
||||
// stream is done, until the service config timeout happens.
|
||||
ctx, cancel = context.WithTimeout(ctx, *mc.Timeout)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
opts = append(cc.dopts.callOptions, opts...)
|
||||
for _, o := range opts {
|
||||
@ -228,6 +222,11 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
|
||||
}()
|
||||
}
|
||||
|
||||
var (
|
||||
t transport.ClientTransport
|
||||
s *transport.Stream
|
||||
done func(balancer.DoneInfo)
|
||||
)
|
||||
for {
|
||||
// Check to make sure the context has expired. This will prevent us from
|
||||
// looping forever if an error occurs for wait-for-ready RPCs where no data
|
||||
@ -283,29 +282,17 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
|
||||
statsHandler: cc.dopts.copts.StatsHandler,
|
||||
}
|
||||
if desc != unaryStreamDesc {
|
||||
// Listen on s.Context().Done() to detect cancellation and s.Done() to
|
||||
// detect normal termination when there is no pending I/O operations on
|
||||
// this stream. Not necessary for "unary" streams, since we are guaranteed
|
||||
// to always be in stream functions.
|
||||
// Listen on cc and stream contexts to cleanup when the user closes the
|
||||
// ClientConn or cancels the stream context. In all other cases, an error
|
||||
// should already be injected into the recv buffer by the transport, which
|
||||
// the client will eventually receive, and then we will cancel the stream's
|
||||
// context in clientStream.finish.
|
||||
go func() {
|
||||
select {
|
||||
case <-t.Error():
|
||||
// Incur transport error, simply exit.
|
||||
case <-cc.ctx.Done():
|
||||
cs.finish(ErrClientConnClosing)
|
||||
cs.closeTransportStream(ErrClientConnClosing)
|
||||
case <-s.Done():
|
||||
// TODO: The trace of the RPC is terminated here when there is no pending
|
||||
// I/O, which is probably not the optimal solution.
|
||||
cs.finish(s.Status().Err())
|
||||
cs.closeTransportStream(nil)
|
||||
case <-s.GoAway():
|
||||
cs.finish(errConnDrain)
|
||||
cs.closeTransportStream(errConnDrain)
|
||||
case <-s.Context().Done():
|
||||
err := s.Context().Err()
|
||||
cs.finish(err)
|
||||
cs.closeTransportStream(transport.ContextErr(err))
|
||||
case <-ctx.Done():
|
||||
cs.finish(toRPCErr(s.Context().Err()))
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -337,7 +324,6 @@ type clientStream struct {
|
||||
mu sync.Mutex
|
||||
done func(balancer.DoneInfo)
|
||||
sentLast bool // sent an end stream
|
||||
closed bool
|
||||
finished bool
|
||||
// trInfo.tr is set when the clientStream is created (if EnableTracing is true),
|
||||
// and is set to nil when the clientStream's finish method is called.
|
||||
@ -357,9 +343,8 @@ func (cs *clientStream) Context() context.Context {
|
||||
func (cs *clientStream) Header() (metadata.MD, error) {
|
||||
m, err := cs.s.Header()
|
||||
if err != nil {
|
||||
if _, ok := err.(transport.ConnectionError); !ok {
|
||||
cs.closeTransportStream(err)
|
||||
}
|
||||
err = toRPCErr(err)
|
||||
cs.finish(err)
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
@ -380,23 +365,18 @@ func (cs *clientStream) SendMsg(m interface{}) (err error) {
|
||||
// TODO Investigate how to signal the stats handling party.
|
||||
// generate error stats if err != nil && err != io.EOF?
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
// For non-client-streaming RPCs, we return nil instead of EOF on success
|
||||
// because the generated code requires it. finish is not called; RecvMsg()
|
||||
// will call it with the stream's status independently.
|
||||
if err == io.EOF && !cs.desc.ClientStreams {
|
||||
err = nil
|
||||
}
|
||||
cs.finish(err)
|
||||
if err == io.EOF {
|
||||
// SendMsg is only called once for non-client-streams. io.EOF needs to be
|
||||
// skipped when the rpc is early finished (before the stream object is
|
||||
// created.).
|
||||
if !cs.desc.ClientStreams {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
if err != nil && err != io.EOF {
|
||||
// Call finish for errors generated by this SendMsg call. (Transport
|
||||
// errors are converted to an io.EOF error below; the real error will be
|
||||
// returned from RecvMsg eventually in that case.)
|
||||
cs.finish(err)
|
||||
}
|
||||
if _, ok := err.(transport.ConnectionError); !ok {
|
||||
cs.closeTransportStream(err)
|
||||
}
|
||||
err = toRPCErr(err)
|
||||
}()
|
||||
var outPayload *stats.OutPayload
|
||||
if cs.statsHandler != nil {
|
||||
@ -408,9 +388,6 @@ func (cs *clientStream) SendMsg(m interface{}) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cs.c.maxSendMessageSize == nil {
|
||||
return status.Errorf(codes.Internal, "callInfo maxSendMessageSize field uninitialized(nil)")
|
||||
}
|
||||
if len(data) > *cs.c.maxSendMessageSize {
|
||||
return status.Errorf(codes.ResourceExhausted, "trying to send message larger than max (%d vs. %d)", len(data), *cs.c.maxSendMessageSize)
|
||||
}
|
||||
@ -418,21 +395,21 @@ func (cs *clientStream) SendMsg(m interface{}) (err error) {
|
||||
cs.sentLast = true
|
||||
}
|
||||
err = cs.t.Write(cs.s, hdr, data, &transport.Options{Last: !cs.desc.ClientStreams})
|
||||
if err == nil && outPayload != nil {
|
||||
outPayload.SentTime = time.Now()
|
||||
cs.statsHandler.HandleRPC(cs.statsCtx, outPayload)
|
||||
if err == nil {
|
||||
if outPayload != nil {
|
||||
outPayload.SentTime = time.Now()
|
||||
cs.statsHandler.HandleRPC(cs.statsCtx, outPayload)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func (cs *clientStream) RecvMsg(m interface{}) (err error) {
|
||||
defer func() {
|
||||
// err != nil indicates the termination of the stream.
|
||||
if err != nil {
|
||||
if err != nil || !cs.desc.ServerStreams {
|
||||
// err != nil or non-server-streaming indicates end of stream.
|
||||
cs.finish(err)
|
||||
if cs.cancel != nil {
|
||||
cs.cancel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
var inPayload *stats.InPayload
|
||||
@ -441,9 +418,6 @@ func (cs *clientStream) RecvMsg(m interface{}) (err error) {
|
||||
Client: true,
|
||||
}
|
||||
}
|
||||
if cs.c.maxReceiveMessageSize == nil {
|
||||
return status.Errorf(codes.Internal, "callInfo maxReceiveMessageSize field uninitialized(nil)")
|
||||
}
|
||||
if !cs.decompSet {
|
||||
// Block until we receive headers containing received message encoding.
|
||||
if ct := cs.s.RecvCompress(); ct != "" && ct != encoding.Identity {
|
||||
@ -462,14 +436,11 @@ func (cs *clientStream) RecvMsg(m interface{}) (err error) {
|
||||
}
|
||||
err = recv(cs.p, cs.codec, cs.s, cs.dc, m, *cs.c.maxReceiveMessageSize, inPayload, cs.decomp)
|
||||
if err != nil {
|
||||
if _, ok := err.(transport.ConnectionError); !ok {
|
||||
cs.closeTransportStream(err)
|
||||
}
|
||||
if err == io.EOF {
|
||||
if statusErr := cs.s.Status().Err(); statusErr != nil {
|
||||
return statusErr
|
||||
}
|
||||
return io.EOF // indicates end of stream.
|
||||
return io.EOF // indicates successful end of stream.
|
||||
}
|
||||
return toRPCErr(err)
|
||||
}
|
||||
@ -484,22 +455,18 @@ func (cs *clientStream) RecvMsg(m interface{}) (err error) {
|
||||
cs.statsHandler.HandleRPC(cs.statsCtx, inPayload)
|
||||
}
|
||||
if cs.desc.ServerStreams {
|
||||
// Subsequent messages should be received by subsequent RecvMsg calls.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Special handling for non-server-stream rpcs.
|
||||
// This recv expects EOF or errors, so we don't collect inPayload.
|
||||
err = recv(cs.p, cs.codec, cs.s, cs.dc, m, *cs.c.maxReceiveMessageSize, nil, cs.decomp)
|
||||
cs.closeTransportStream(err)
|
||||
if err == nil {
|
||||
return toRPCErr(errors.New("grpc: client streaming protocol violation: get <nil>, want <EOF>"))
|
||||
}
|
||||
if err == io.EOF {
|
||||
if se := cs.s.Status().Err(); se != nil {
|
||||
return se
|
||||
}
|
||||
cs.finish(err)
|
||||
return nil
|
||||
return cs.s.Status().Err() // non-server streaming Recv returns nil on success
|
||||
}
|
||||
return toRPCErr(err)
|
||||
}
|
||||
@ -509,41 +476,26 @@ func (cs *clientStream) CloseSend() error {
|
||||
return nil
|
||||
}
|
||||
cs.sentLast = true
|
||||
err := cs.t.Write(cs.s, nil, nil, &transport.Options{Last: true})
|
||||
if err == nil || err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if _, ok := err.(transport.ConnectionError); !ok {
|
||||
cs.closeTransportStream(err)
|
||||
}
|
||||
err = toRPCErr(err)
|
||||
if err != nil {
|
||||
cs.finish(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (cs *clientStream) closeTransportStream(err error) {
|
||||
cs.mu.Lock()
|
||||
if cs.closed {
|
||||
cs.mu.Unlock()
|
||||
return
|
||||
}
|
||||
cs.closed = true
|
||||
cs.mu.Unlock()
|
||||
cs.t.CloseStream(cs.s, err)
|
||||
cs.t.Write(cs.s, nil, nil, &transport.Options{Last: true})
|
||||
// We ignore errors from Write and always return nil here. Any error it
|
||||
// would return would also be returned by a subsequent RecvMsg call, and the
|
||||
// user is supposed to always finish the stream by calling RecvMsg until it
|
||||
// returns err != nil.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *clientStream) finish(err error) {
|
||||
// Do not call cs.cancel in this function. Only call it when RecvMag()
|
||||
// returns non-nil error because of
|
||||
// https://github.com/grpc/grpc-go/issues/1818.
|
||||
if err == io.EOF {
|
||||
// Ending a stream with EOF indicates a success.
|
||||
err = nil
|
||||
}
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
if cs.finished {
|
||||
return
|
||||
}
|
||||
cs.finished = true
|
||||
cs.t.CloseStream(cs.s, err)
|
||||
for _, o := range cs.opts {
|
||||
o.after(cs.c)
|
||||
}
|
||||
@ -559,18 +511,16 @@ func (cs *clientStream) finish(err error) {
|
||||
end := &stats.End{
|
||||
Client: true,
|
||||
EndTime: time.Now(),
|
||||
}
|
||||
if err != io.EOF {
|
||||
// end.Error is nil if the RPC finished successfully.
|
||||
end.Error = toRPCErr(err)
|
||||
Error: err,
|
||||
}
|
||||
cs.statsHandler.HandleRPC(cs.statsCtx, end)
|
||||
}
|
||||
cs.cancel()
|
||||
if !cs.tracing {
|
||||
return
|
||||
}
|
||||
if cs.trInfo.tr != nil {
|
||||
if err == nil || err == io.EOF {
|
||||
if err == nil {
|
||||
cs.trInfo.tr.LazyPrintf("RPC: [OK]")
|
||||
} else {
|
||||
cs.trInfo.tr.LazyPrintf("RPC: [%v]", err)
|
||||
|
||||
Reference in New Issue
Block a user