Skip to content

Commit 3b0ab98

Browse files
committed
quic: avoid deadlock on listener close
Avoid holding Listener.connsMu while blocking on a Conn's loop, since the Conn can acquire the mutex while shutting down. Fix Conn.waitReady to check conn readiness before checking the Context doneness. This doesn't make a difference in the current exported API, but this simplifies some tests and will be useful once 0-RTT is implemented. Refactor a bit of the testConn datagram handling to use a testListener type, which helped expose the above deadlock and will be useful for writing tests which don't involve a Conn. Change-Id: I064fca99ae9a165631fc0ff46eb334d25d7dd957 Reviewed-on: https://go-review.googlesource.com/c/net/+/529935 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 732b4bc commit 3b0ab98

File tree

4 files changed

+100
-38
lines changed

4 files changed

+100
-38
lines changed

internal/quic/conn_close.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ func (c *Conn) enterDraining(err error) {
168168
}
169169

170170
func (c *Conn) waitReady(ctx context.Context) error {
171+
select {
172+
case <-c.lifetime.readyc:
173+
return nil
174+
case <-c.lifetime.drainingc:
175+
return c.lifetime.finalErr
176+
default:
177+
}
171178
select {
172179
case <-c.lifetime.readyc:
173180
return nil
@@ -215,7 +222,7 @@ func (c *Conn) Abort(err error) {
215222
if err == nil {
216223
err = localTransportError(errNo)
217224
}
218-
c.runOnLoop(func(now time.Time, c *Conn) {
225+
c.sendMsg(func(now time.Time, c *Conn) {
219226
c.abort(now, err)
220227
})
221228
}
@@ -228,11 +235,18 @@ func (c *Conn) abort(now time.Time, err error) {
228235
c.lifetime.localErr = err
229236
}
230237

238+
// abortImmediately terminates a connection.
239+
// The connection does not send a CONNECTION_CLOSE, and skips the draining period.
240+
func (c *Conn) abortImmediately(now time.Time, err error) {
241+
c.abort(now, err)
242+
c.enterDraining(err)
243+
c.exited = true
244+
}
245+
231246
// exit fully terminates a connection immediately.
232247
func (c *Conn) exit() {
233-
c.runOnLoop(func(now time.Time, c *Conn) {
248+
c.sendMsg(func(now time.Time, c *Conn) {
234249
c.enterDraining(errors.New("connection closed"))
235250
c.exited = true
236251
})
237-
<-c.donec
238252
}

internal/quic/conn_test.go

+7-34
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ import (
1313
"errors"
1414
"flag"
1515
"fmt"
16-
"io"
1716
"math"
18-
"net"
1917
"net/netip"
2018
"reflect"
2119
"strings"
@@ -112,7 +110,7 @@ const maxTestKeyPhases = 3
112110
type testConn struct {
113111
t *testing.T
114112
conn *Conn
115-
listener *Listener
113+
listener *testListener
116114
now time.Time
117115
timer time.Time
118116
timerLastFired time.Time
@@ -231,8 +229,8 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn {
231229
tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams))
232230
tc.peerTLSConn.Start(context.Background())
233231

234-
tc.listener = newListener((*testConnUDPConn)(tc), config, (*testConnHooks)(tc))
235-
conn, err := tc.listener.newConn(
232+
tc.listener = newTestListener(t, config, (*testConnHooks)(tc))
233+
conn, err := tc.listener.l.newConn(
236234
tc.now,
237235
side,
238236
initialConnID,
@@ -335,7 +333,7 @@ func (tc *testConn) cleanup() {
335333
return
336334
}
337335
tc.conn.exit()
338-
tc.listener.Close(context.Background())
336+
<-tc.conn.donec
339337
}
340338

341339
func (tc *testConn) logDatagram(text string, d *testDatagram) {
@@ -388,6 +386,7 @@ func (tc *testConn) write(d *testDatagram) {
388386
for len(buf) < d.paddedSize {
389387
buf = append(buf, 0)
390388
}
389+
// TODO: This should use tc.listener.write.
391390
tc.conn.sendMsg(&datagram{
392391
b: buf,
393392
})
@@ -457,11 +456,10 @@ func (tc *testConn) readDatagram() *testDatagram {
457456
tc.wait()
458457
tc.sentPackets = nil
459458
tc.sentFrames = nil
460-
if len(tc.sentDatagrams) == 0 {
459+
buf := tc.listener.read()
460+
if buf == nil {
461461
return nil
462462
}
463-
buf := tc.sentDatagrams[0]
464-
tc.sentDatagrams = tc.sentDatagrams[1:]
465463
d := tc.parseTestDatagram(buf)
466464
// Log the datagram before removing ignored frames.
467465
// When things go wrong, it's useful to see all the frames.
@@ -982,31 +980,6 @@ func testPeerConnID(seq int64) []byte {
982980
return []byte{0xbe, 0xee, 0xff, byte(seq)}
983981
}
984982

985-
// testConnUDPConn implements UDPConn.
986-
type testConnUDPConn testConn
987-
988-
func (tc *testConnUDPConn) Close() error {
989-
close(tc.recvDatagram)
990-
return nil
991-
}
992-
993-
func (tc *testConnUDPConn) LocalAddr() net.Addr {
994-
return net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443"))
995-
}
996-
997-
func (tc *testConnUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) {
998-
for d := range tc.recvDatagram {
999-
n = copy(b, d.b)
1000-
return n, 0, 0, d.addr, nil
1001-
}
1002-
return 0, 0, 0, netip.AddrPort{}, io.EOF
1003-
}
1004-
1005-
func (tc *testConnUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
1006-
tc.sentDatagrams = append(tc.sentDatagrams, append([]byte(nil), b...))
1007-
return len(b), nil
1008-
}
1009-
1010983
// canceledContext returns a canceled Context.
1011984
//
1012985
// Functions which take a context preference progress over cancelation.

internal/quic/listener.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (l *Listener) Close(ctx context.Context) error {
104104
if !l.closing {
105105
l.closing = true
106106
for c := range l.conns {
107-
c.Close()
107+
c.Abort(errors.New("listener closed"))
108108
}
109109
if len(l.conns) == 0 {
110110
l.udpConn.Close()

internal/quic/listener_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"bytes"
1111
"context"
1212
"io"
13+
"net"
14+
"net/netip"
1315
"testing"
1416
)
1517

@@ -86,3 +88,76 @@ func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener {
8688
})
8789
return l
8890
}
91+
92+
type testListener struct {
93+
t *testing.T
94+
l *Listener
95+
recvc chan *datagram
96+
idlec chan struct{}
97+
sentDatagrams [][]byte
98+
}
99+
100+
func newTestListener(t *testing.T, config *Config, testHooks connTestHooks) *testListener {
101+
tl := &testListener{
102+
t: t,
103+
recvc: make(chan *datagram),
104+
idlec: make(chan struct{}),
105+
}
106+
tl.l = newListener((*testListenerUDPConn)(tl), config, testHooks)
107+
t.Cleanup(tl.cleanup)
108+
return tl
109+
}
110+
111+
func (tl *testListener) cleanup() {
112+
tl.l.Close(canceledContext())
113+
}
114+
115+
func (tl *testListener) wait() {
116+
tl.idlec <- struct{}{}
117+
}
118+
119+
func (tl *testListener) write(d *datagram) {
120+
tl.recvc <- d
121+
tl.wait()
122+
}
123+
124+
func (tl *testListener) read() []byte {
125+
tl.wait()
126+
if len(tl.sentDatagrams) == 0 {
127+
return nil
128+
}
129+
d := tl.sentDatagrams[0]
130+
tl.sentDatagrams = tl.sentDatagrams[1:]
131+
return d
132+
}
133+
134+
// testListenerUDPConn implements UDPConn.
135+
type testListenerUDPConn testListener
136+
137+
func (tl *testListenerUDPConn) Close() error {
138+
close(tl.recvc)
139+
return nil
140+
}
141+
142+
func (tl *testListenerUDPConn) LocalAddr() net.Addr {
143+
return net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443"))
144+
}
145+
146+
func (tl *testListenerUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) {
147+
for {
148+
select {
149+
case d, ok := <-tl.recvc:
150+
if !ok {
151+
return 0, 0, 0, netip.AddrPort{}, io.EOF
152+
}
153+
n = copy(b, d.b)
154+
return n, 0, 0, d.addr, nil
155+
case <-tl.idlec:
156+
}
157+
}
158+
}
159+
160+
func (tl *testListenerUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
161+
tl.sentDatagrams = append(tl.sentDatagrams, append([]byte(nil), b...))
162+
return len(b), nil
163+
}

0 commit comments

Comments
 (0)