Skip to content

Commit 9373e5c

Browse files
authored
transport: Fix closing a closed channel panic in handlePing (#5854)
1 parent 2f413c4 commit 9373e5c

File tree

3 files changed

+77
-12
lines changed

3 files changed

+77
-12
lines changed

internal/transport/http2_server.go

+13-12
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"google.golang.org/grpc/credentials"
4343
"google.golang.org/grpc/internal/channelz"
4444
"google.golang.org/grpc/internal/grpcrand"
45+
"google.golang.org/grpc/internal/grpcsync"
4546
"google.golang.org/grpc/keepalive"
4647
"google.golang.org/grpc/metadata"
4748
"google.golang.org/grpc/peer"
@@ -102,13 +103,13 @@ type http2Server struct {
102103

103104
mu sync.Mutex // guard the following
104105

105-
// drainChan is initialized when Drain() is called the first time.
106-
// After which the server writes out the first GoAway(with ID 2^31-1) frame.
107-
// Then an independent goroutine will be launched to later send the second GoAway.
108-
// During this time we don't want to write another first GoAway(with ID 2^31 -1) frame.
109-
// Thus call to Drain() will be a no-op if drainChan is already initialized since draining is
110-
// already underway.
111-
drainChan chan struct{}
106+
// drainEvent is initialized when Drain() is called the first time. After
107+
// which the server writes out the first GoAway(with ID 2^31-1) frame. Then
108+
// an independent goroutine will be launched to later send the second
109+
// GoAway. During this time we don't want to write another first GoAway(with
110+
// ID 2^31 -1) frame. Thus call to Drain() will be a no-op if drainEvent is
111+
// already initialized since draining is already underway.
112+
drainEvent *grpcsync.Event
112113
state transportState
113114
activeStreams map[uint32]*Stream
114115
// idle is the time instant when the connection went idle.
@@ -838,8 +839,8 @@ const (
838839

839840
func (t *http2Server) handlePing(f *http2.PingFrame) {
840841
if f.IsAck() {
841-
if f.Data == goAwayPing.data && t.drainChan != nil {
842-
close(t.drainChan)
842+
if f.Data == goAwayPing.data && t.drainEvent != nil {
843+
t.drainEvent.Fire()
843844
return
844845
}
845846
// Maybe it's a BDP ping.
@@ -1287,10 +1288,10 @@ func (t *http2Server) RemoteAddr() net.Addr {
12871288
func (t *http2Server) Drain() {
12881289
t.mu.Lock()
12891290
defer t.mu.Unlock()
1290-
if t.drainChan != nil {
1291+
if t.drainEvent != nil {
12911292
return
12921293
}
1293-
t.drainChan = make(chan struct{})
1294+
t.drainEvent = grpcsync.NewEvent()
12941295
t.controlBuf.put(&goAway{code: http2.ErrCodeNo, debugData: []byte{}, headsUp: true})
12951296
}
12961297

@@ -1346,7 +1347,7 @@ func (t *http2Server) outgoingGoAwayHandler(g *goAway) (bool, error) {
13461347
timer := time.NewTimer(time.Minute)
13471348
defer timer.Stop()
13481349
select {
1349-
case <-t.drainChan:
1350+
case <-t.drainEvent.Done():
13501351
case <-timer.C:
13511352
case <-t.done:
13521353
return

test/goaway_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ func testServerMultipleGoAwayPendingRPC(t *testing.T, e env) {
363363
close(ch2)
364364
}()
365365
// Loop until the server side GoAway signal is propagated to the client.
366+
366367
for {
367368
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
368369
if _, err := tc.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
@@ -402,6 +403,7 @@ func testServerMultipleGoAwayPendingRPC(t *testing.T, e env) {
402403
if err := stream.CloseSend(); err != nil {
403404
t.Fatalf("%v.CloseSend() = %v, want <nil>", stream, err)
404405
}
406+
405407
<-ch1
406408
<-ch2
407409
cancel()
@@ -707,3 +709,59 @@ func (s) TestGoAwayStreamIDSmallerThanCreatedStreams(t *testing.T) {
707709
ct.writeGoAway(1, http2.ErrCodeNo, []byte{})
708710
goAwayWritten.Fire()
709711
}
712+
713+
// TestTwoGoAwayPingFrames tests the scenario where you get two go away ping
714+
// frames from the client during graceful shutdown. This should not crash the
715+
// server.
716+
func (s) TestTwoGoAwayPingFrames(t *testing.T) {
717+
lis, err := net.Listen("tcp", "localhost:0")
718+
if err != nil {
719+
t.Fatalf("Failed to listen: %v", err)
720+
}
721+
defer lis.Close()
722+
s := grpc.NewServer()
723+
defer s.Stop()
724+
go s.Serve(lis)
725+
726+
conn, err := net.DialTimeout("tcp", lis.Addr().String(), defaultTestTimeout)
727+
if err != nil {
728+
t.Fatalf("Failed to dial: %v", err)
729+
}
730+
731+
st := newServerTesterFromConn(t, conn)
732+
st.greet()
733+
pingReceivedClientSide := testutils.NewChannel()
734+
go func() {
735+
for {
736+
f, err := st.readFrame()
737+
if err != nil {
738+
return
739+
}
740+
switch f.(type) {
741+
case *http2.GoAwayFrame:
742+
case *http2.PingFrame:
743+
pingReceivedClientSide.Send(nil)
744+
default:
745+
t.Errorf("server tester received unexpected frame type %T", f)
746+
}
747+
}
748+
}()
749+
gsDone := testutils.NewChannel()
750+
go func() {
751+
s.GracefulStop()
752+
gsDone.Send(nil)
753+
}()
754+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
755+
defer cancel()
756+
if _, err := pingReceivedClientSide.Receive(ctx); err != nil {
757+
t.Fatalf("Error waiting for ping frame client side from graceful shutdown: %v", err)
758+
}
759+
// Write two goaway pings here.
760+
st.writePing(true, [8]byte{1, 6, 1, 8, 0, 3, 3, 9})
761+
st.writePing(true, [8]byte{1, 6, 1, 8, 0, 3, 3, 9})
762+
// Close the conn to finish up the Graceful Shutdown process.
763+
conn.Close()
764+
if _, err := gsDone.Receive(ctx); err != nil {
765+
t.Fatalf("Error waiting for graceful shutdown of the server: %v", err)
766+
}
767+
}

test/servertester.go

+6
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,9 @@ func (st *serverTester) writeRSTStream(streamID uint32, code http2.ErrCode) {
273273
st.t.Fatalf("Error writing RST_STREAM: %v", err)
274274
}
275275
}
276+
277+
func (st *serverTester) writePing(ack bool, data [8]byte) {
278+
if err := st.fr.WritePing(ack, data); err != nil {
279+
st.t.Fatalf("Error writing PING: %v", err)
280+
}
281+
}

0 commit comments

Comments
 (0)