Skip to content

Commit

Permalink
server: add missing conn.Close if the connection dies before reading …
Browse files Browse the repository at this point in the history
…the HTTP/2 preface (#4837)
  • Loading branch information
dfawley committed Oct 4, 2021
1 parent 0997020 commit f068a13
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 7 deletions.
7 changes: 4 additions & 3 deletions internal/transport/http2_server.go
Expand Up @@ -290,10 +290,11 @@ func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport,
if _, err := io.ReadFull(t.conn, preface); err != nil {
// In deployments where a gRPC server runs behind a cloud load balancer
// which performs regular TCP level health checks, the connection is
// closed immediately by the latter. Skipping the error here will help
// reduce log clutter.
// closed immediately by the latter. Returning io.EOF here allows the
// grpc server implementation to recognize this scenario and suppress
// logging to reduce spam.
if err == io.EOF {
return nil, nil
return nil, io.EOF
}
return nil, connectionErrorf(false, err, "transport: http2Server.HandleStreams failed to receive the preface from client: %v", err)
}
Expand Down
6 changes: 2 additions & 4 deletions server.go
Expand Up @@ -885,13 +885,11 @@ func (s *Server) newHTTP2Transport(c net.Conn) transport.ServerTransport {
// ErrConnDispatched means that the connection was dispatched away from
// gRPC; those connections should be left open.
if err != credentials.ErrConnDispatched {
c.Close()
}
// Don't log on ErrConnDispatched and io.EOF to prevent log spam.
if err != credentials.ErrConnDispatched {
// Don't log on ErrConnDispatched and io.EOF to prevent log spam.
if err != io.EOF {
channelz.Warning(logger, s.channelzID, "grpc: Server.Serve failed to create ServerTransport: ", err)
}
c.Close()
}
return nil
}
Expand Down
58 changes: 58 additions & 0 deletions test/end2end_test.go
Expand Up @@ -7990,3 +7990,61 @@ func (s) TestAuthorityHeader(t *testing.T) {
})
}
}

// wrapCloseListener tracks Accepts/Closes and maintains a counter of the
// number of open connections.
type wrapCloseListener struct {
net.Listener
connsOpen int32
}

// wrapCloseListener is returned by wrapCloseListener.Accept and decrements its
// connsOpen when Close is called.
type wrapCloseConn struct {
net.Conn
lis *wrapCloseListener
closeOnce sync.Once
}

func (w *wrapCloseListener) Accept() (net.Conn, error) {
conn, err := w.Listener.Accept()
if err != nil {
return nil, err
}
atomic.AddInt32(&w.connsOpen, 1)
return &wrapCloseConn{Conn: conn, lis: w}, nil
}

func (w *wrapCloseConn) Close() error {
defer w.closeOnce.Do(func() { atomic.AddInt32(&w.lis.connsOpen, -1) })
return w.Conn.Close()
}

// TestServerClosesConn ensures conn.Close is always closed even if the client
// doesn't complete the HTTP/2 handshake.
func (s) TestServerClosesConn(t *testing.T) {
lis := bufconn.Listen(20)
wrapLis := &wrapCloseListener{Listener: lis}

s := grpc.NewServer()
go s.Serve(wrapLis)
defer s.Stop()

ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()

for i := 0; i < 10; i++ {
conn, err := lis.DialContext(ctx)
if err != nil {
t.Fatalf("Dial = _, %v; want _, nil", err)
}
conn.Close()
}
for ctx.Err() == nil {
if atomic.LoadInt32(&wrapLis.connsOpen) == 0 {
return
}
time.Sleep(50 * time.Millisecond)
}
t.Fatalf("timed out waiting for conns to be closed by server; still open: %v", atomic.LoadInt32(&wrapLis.connsOpen))
}

0 comments on commit f068a13

Please sign in to comment.