Skip to content

Commit 62292e8

Browse files
committed
http2: detect write-blocked PING frames
We start the PingTimeout timer before writing a PING frame. However, writing the frame can block indefinitely (either acquiring cc.wmu or writing to the conn), and the timer is not checked until after the frame is written. Move PING writes into a separate goroutine, so we can detect write-blocked connections. Fixes golang/go#48810. Change-Id: Ifd67fa23799e750d02754e1fe5d32719f60faed4 Reviewed-on: https://go-review.googlesource.com/c/net/+/354389 Trust: Damien Neil <[email protected]> Run-TryBot: Damien Neil <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent d2e5035 commit 62292e8

File tree

2 files changed

+45
-10
lines changed

2 files changed

+45
-10
lines changed

http2/transport.go

+15-10
Original file line numberDiff line numberDiff line change
@@ -2637,19 +2637,24 @@ func (cc *ClientConn) Ping(ctx context.Context) error {
26372637
}
26382638
cc.mu.Unlock()
26392639
}
2640-
cc.wmu.Lock()
2641-
if err := cc.fr.WritePing(false, p); err != nil {
2642-
cc.wmu.Unlock()
2643-
return err
2644-
}
2645-
if err := cc.bw.Flush(); err != nil {
2646-
cc.wmu.Unlock()
2647-
return err
2648-
}
2649-
cc.wmu.Unlock()
2640+
errc := make(chan error, 1)
2641+
go func() {
2642+
cc.wmu.Lock()
2643+
defer cc.wmu.Unlock()
2644+
if err := cc.fr.WritePing(false, p); err != nil {
2645+
errc <- err
2646+
return
2647+
}
2648+
if err := cc.bw.Flush(); err != nil {
2649+
errc <- err
2650+
return
2651+
}
2652+
}()
26502653
select {
26512654
case <-c:
26522655
return nil
2656+
case err := <-errc:
2657+
return err
26532658
case <-ctx.Done():
26542659
return ctx.Err()
26552660
case <-cc.readerDone:

http2/transport_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -3494,6 +3494,36 @@ func TestTransportCloseAfterLostPing(t *testing.T) {
34943494
ct.run()
34953495
}
34963496

3497+
func TestTransportPingWriteBlocks(t *testing.T) {
3498+
st := newServerTester(t,
3499+
func(w http.ResponseWriter, r *http.Request) {},
3500+
optOnlyServer,
3501+
)
3502+
defer st.Close()
3503+
tr := &Transport{
3504+
TLSClientConfig: tlsConfigInsecure,
3505+
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
3506+
s, c := net.Pipe() // unbuffered, unlike a TCP conn
3507+
go func() {
3508+
// Read initial handshake frames.
3509+
// Without this, we block indefinitely in newClientConn,
3510+
// and never get to the point of sending a PING.
3511+
var buf [1024]byte
3512+
s.Read(buf[:])
3513+
}()
3514+
return c, nil
3515+
},
3516+
PingTimeout: 1 * time.Millisecond,
3517+
ReadIdleTimeout: 1 * time.Millisecond,
3518+
}
3519+
defer tr.CloseIdleConnections()
3520+
c := &http.Client{Transport: tr}
3521+
_, err := c.Get(st.ts.URL)
3522+
if err == nil {
3523+
t.Fatalf("Get = nil, want error")
3524+
}
3525+
}
3526+
34973527
func TestTransportPingWhenReading(t *testing.T) {
34983528
testCases := []struct {
34993529
name string

0 commit comments

Comments
 (0)