Skip to content

Commit 6247a08

Browse files
gjermundgarabadevgianlu
authored andcommitted
fix: unblock accesspoint close during in-flight send
1 parent cde7e29 commit 6247a08

3 files changed

Lines changed: 61 additions & 87 deletions

File tree

ap/ap.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,14 @@ type Accesspoint struct {
5252
encConn *shannonConn
5353

5454
done chan struct{}
55+
closeOnce sync.Once
5556
recvLoopOnce sync.Once
5657
recvChans map[PacketType][]chan Packet
5758
recvChansLock sync.RWMutex
5859
lastPongAck time.Time
5960
lastPongAckLock sync.Mutex
6061

61-
// connMu is held for writing when performing reconnection and for reading mainly when accessing welcome
62-
// or sending packets. If it's not held, a valid connection (and APWelcome) is available. Be careful not to deadlock
63-
// anything with this.
62+
// connMu protects conn, encConn, and welcome pointer state.
6463
connMu sync.RWMutex
6564
welcome *pb.APWelcome
6665
}
@@ -243,30 +242,34 @@ func (ap *Accesspoint) connect(ctx context.Context, creds *pb.LoginCredentials)
243242
}
244243

245244
func (ap *Accesspoint) Close() {
246-
ap.connMu.Lock()
247-
defer ap.connMu.Unlock()
248-
249-
select {
250-
case <-ap.done:
251-
return
252-
default:
253-
}
254-
255-
close(ap.done)
256-
ap.closeConnLocked()
245+
ap.closeOnce.Do(func() {
246+
close(ap.done)
247+
ap.closeConn()
248+
})
257249
}
258250

259251
func (ap *Accesspoint) Send(ctx context.Context, pktType PacketType, payload []byte) error {
260252
ap.connMu.RLock()
261-
defer ap.connMu.RUnlock()
262-
263253
select {
264254
case <-ap.done:
255+
ap.connMu.RUnlock()
265256
return ErrAccesspointClosed
266257
default:
267258
}
268259

269-
return ap.encConn.sendPacket(ctx, pktType, payload)
260+
encConn := ap.encConn
261+
ap.connMu.RUnlock()
262+
263+
if err := encConn.sendPacket(ctx, pktType, payload); err != nil {
264+
select {
265+
case <-ap.done:
266+
return ErrAccesspointClosed
267+
default:
268+
return err
269+
}
270+
}
271+
272+
return nil
270273
}
271274

272275
func (ap *Accesspoint) Receive(types ...PacketType) <-chan Packet {
@@ -453,14 +456,12 @@ func (ap *Accesspoint) timeSinceLastPongAck() time.Duration {
453456
}
454457

455458
func (ap *Accesspoint) closeConn() {
456-
ap.connMu.Lock()
457-
ap.closeConnLocked()
458-
ap.connMu.Unlock()
459-
}
459+
ap.connMu.RLock()
460+
conn := ap.conn
461+
ap.connMu.RUnlock()
460462

461-
func (ap *Accesspoint) closeConnLocked() {
462-
if ap.conn != nil {
463-
_ = ap.conn.Close()
463+
if conn != nil {
464+
_ = conn.Close()
464465
}
465466
}
466467

ap/ap_test.go

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package ap
22

33
import (
44
"context"
5+
"errors"
56
"io"
67
"net"
7-
"sync"
88
"testing"
99
"testing/synctest"
1010
"time"
@@ -17,36 +17,15 @@ type stubAddr string
1717
func (a stubAddr) Network() string { return string(a) }
1818
func (a stubAddr) String() string { return string(a) }
1919

20-
type countingConn struct {
21-
mu sync.Mutex
22-
closes int
23-
}
24-
25-
func (c *countingConn) Read([]byte) (int, error) { return 0, io.EOF }
26-
func (c *countingConn) Write(b []byte) (int, error) { return len(b), nil }
27-
func (c *countingConn) Close() error { c.mu.Lock(); c.closes++; c.mu.Unlock(); return nil }
28-
func (c *countingConn) LocalAddr() net.Addr { return stubAddr("local") }
29-
func (c *countingConn) RemoteAddr() net.Addr { return stubAddr("remote") }
30-
func (c *countingConn) SetDeadline(time.Time) error { return nil }
31-
func (c *countingConn) SetReadDeadline(time.Time) error { return nil }
32-
func (c *countingConn) SetWriteDeadline(time.Time) error { return nil }
33-
34-
func (c *countingConn) CloseCount() int {
35-
c.mu.Lock()
36-
defer c.mu.Unlock()
37-
return c.closes
38-
}
39-
4020
type blockingConn struct {
41-
started sync.Once
42-
startCh chan struct{}
43-
blockCh chan struct{}
21+
startCh chan struct{}
22+
closedCh chan struct{}
4423
}
4524

4625
func newBlockingConn() *blockingConn {
4726
return &blockingConn{
48-
startCh: make(chan struct{}),
49-
blockCh: make(chan struct{}),
27+
startCh: make(chan struct{}),
28+
closedCh: make(chan struct{}),
5029
}
5130
}
5231

@@ -57,13 +36,14 @@ func (c *blockingConn) SetDeadline(time.Time) error { return nil }
5736
func (c *blockingConn) SetReadDeadline(time.Time) error { return nil }
5837
func (c *blockingConn) SetWriteDeadline(time.Time) error { return nil }
5938

60-
func (c *blockingConn) Write(b []byte) (int, error) {
61-
c.started.Do(func() { close(c.startCh) })
62-
<-c.blockCh
63-
return len(b), nil
39+
func (c *blockingConn) Write([]byte) (int, error) {
40+
close(c.startCh)
41+
<-c.closedCh
42+
return 0, io.ErrClosedPipe
6443
}
6544

6645
func (c *blockingConn) Close() error {
46+
close(c.closedCh)
6747
return nil
6848
}
6949

@@ -117,7 +97,7 @@ func TestDoneChannelClosesOnClose(t *testing.T) {
11797
}
11898
}
11999

120-
func TestCloseWaitsForInFlightSend(t *testing.T) {
100+
func TestCloseSignalsAndUnblocksInFlightSend(t *testing.T) {
121101
conn := newBlockingConn()
122102
ap := NewAccesspoint(&librespot.NullLogger{}, nil, "")
123103
ap.conn = conn
@@ -141,25 +121,23 @@ func TestCloseWaitsForInFlightSend(t *testing.T) {
141121
}()
142122

143123
select {
144-
case <-closeDone:
145-
t.Fatal("close returned before in-flight send finished")
146-
case <-time.After(50 * time.Millisecond):
124+
case <-ap.Done():
125+
case <-time.After(time.Second):
126+
t.Fatal("timed out waiting for done channel to close")
147127
}
148128

149-
close(conn.blockCh)
150-
151129
select {
152-
case err := <-sendDone:
153-
if err != nil {
154-
t.Fatalf("send error = %v", err)
155-
}
130+
case <-closeDone:
156131
case <-time.After(time.Second):
157-
t.Fatal("timed out waiting for send to finish")
132+
t.Fatal("timed out waiting for close to finish")
158133
}
159134

160135
select {
161-
case <-closeDone:
136+
case err := <-sendDone:
137+
if !errors.Is(err, ErrAccesspointClosed) {
138+
t.Fatalf("expected ErrAccesspointClosed, got %v", err)
139+
}
162140
case <-time.After(time.Second):
163-
t.Fatal("timed out waiting for close to finish")
141+
t.Fatal("timed out waiting for send to finish")
164142
}
165143
}

dealer/dealer.go

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ type Dealer struct {
3333
conn *websocket.Conn
3434

3535
done chan struct{}
36+
closeOnce sync.Once
3637
recvLoopOnce sync.Once
3738
lastPong time.Time
3839
lastPongLock sync.Mutex
3940

40-
// connMu is held for writing when performing reconnection and for reading when accessing the conn.
41-
// If it's not held, a valid connection is available. Be careful not to deadlock anything with this.
41+
// connMu protects conn pointer state.
4242
connMu sync.RWMutex
4343

4444
messageReceivers []messageReceiver
@@ -113,21 +113,10 @@ func (d *Dealer) connect(ctx context.Context) error {
113113
}
114114

115115
func (d *Dealer) Close() {
116-
d.connMu.Lock()
117-
select {
118-
case <-d.done:
119-
d.connMu.Unlock()
120-
return
121-
default:
122-
}
123-
124-
close(d.done)
125-
conn := d.conn
126-
d.connMu.Unlock()
127-
128-
if conn != nil {
129-
_ = conn.Close(websocket.StatusGoingAway, "")
130-
}
116+
d.closeOnce.Do(func() {
117+
close(d.done)
118+
d.closeConn(websocket.StatusGoingAway)
119+
})
131120
}
132121

133122
func (d *Dealer) startReceiving() {
@@ -336,7 +325,6 @@ func (d *Dealer) closeConnRef(conn *websocket.Conn, status websocket.StatusCode)
336325

337326
func (d *Dealer) writeConn(ctx context.Context, typ websocket.MessageType, payload []byte) (*websocket.Conn, error) {
338327
d.connMu.RLock()
339-
340328
select {
341329
case <-d.done:
342330
d.connMu.RUnlock()
@@ -345,14 +333,21 @@ func (d *Dealer) writeConn(ctx context.Context, typ websocket.MessageType, paylo
345333
}
346334

347335
conn := d.conn
336+
d.connMu.RUnlock()
348337

349338
if conn == nil {
350-
d.connMu.RUnlock()
351339
return nil, fmt.Errorf("dealer connection not established")
352340
}
353341

354342
err := conn.Write(ctx, typ, payload)
355-
d.connMu.RUnlock()
343+
if err != nil {
344+
select {
345+
case <-d.done:
346+
return conn, ErrDealerClosed
347+
default:
348+
}
349+
}
350+
356351
return conn, err
357352
}
358353

0 commit comments

Comments
 (0)