Skip to content

Commit 23a677d

Browse files
committed
replication: fix BinlogSyncer.Close hang on SSH tunnels
SetReadDeadline unblocks ReadPacket so onStream notices ctx cancellation. Some transports (notably SSH tunnels) refuse deadlines with an error, in which case close the underlying connection to force ReadPacket to return. Otherwise wg.Wait below parks forever when KILL also fails to reach server (e.g. thread already reaped, "ERROR 1094 unknown thread id")
1 parent d0b04ca commit 23a677d

2 files changed

Lines changed: 86 additions & 3 deletions

File tree

replication/binlogsyncer.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,17 @@ func (b *BinlogSyncer) close() {
240240
b.cancel()
241241

242242
if b.c != nil {
243-
err := b.c.SetReadDeadline(utils.Now().Add(100 * time.Millisecond))
244-
if err != nil {
245-
b.cfg.Logger.Warn("could not set read deadline", slog.Any("error", err))
243+
// SetReadDeadline unblocks ReadPacket so onStream notices ctx cancellation.
244+
// Some transports (notably SSH tunnels) refuse deadlines with an error, in
245+
// which case close the underlying connection to force ReadPacket to return.
246+
// Otherwise wg.Wait below parks forever when KILL also fails to reach server
247+
// (e.g. thread already reaped, "ERROR 1094 unknown thread id")
248+
if err := b.c.SetReadDeadline(utils.Now().Add(100 * time.Millisecond)); err != nil {
249+
b.cfg.Logger.Warn("could not set read deadline, closing connection to unblock reader",
250+
slog.Any("error", err))
251+
if err := b.c.Close(); err != nil {
252+
b.cfg.Logger.Warn("could not close connection", slog.Any("error", err))
253+
}
246254
}
247255
}
248256

replication/binlogsyncer_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package replication
22

33
import (
4+
"errors"
5+
"io"
6+
"net"
47
"os"
58
"strings"
9+
"sync"
610
"testing"
11+
"time"
712

813
"github.com/stretchr/testify/require"
14+
15+
"github.com/go-mysql-org/go-mysql/client"
16+
"github.com/go-mysql-org/go-mysql/packet"
917
)
1018

1119
func TestLocalHostname(t *testing.T) {
@@ -48,3 +56,70 @@ func TestLocalHostname_os(t *testing.T) {
4856
h, _ := os.Hostname()
4957
require.Equal(t, h, b.localHostname())
5058
}
59+
60+
// deadlinelessConn mimics the net.Conn surface of an ssh tunneled channel:
61+
// Read blocks until Close, and SetReadDeadline refuses with an error rather
62+
// than interrupting the parked Read.
63+
type deadlinelessConn struct {
64+
closeOnce sync.Once
65+
closed chan struct{}
66+
}
67+
68+
func newDeadlinelessConn() *deadlinelessConn {
69+
return &deadlinelessConn{closed: make(chan struct{})}
70+
}
71+
72+
func (c *deadlinelessConn) Read(b []byte) (int, error) {
73+
<-c.closed
74+
return 0, io.EOF
75+
}
76+
77+
func (c *deadlinelessConn) Write(b []byte) (int, error) { return len(b), nil }
78+
79+
func (c *deadlinelessConn) Close() error {
80+
c.closeOnce.Do(func() { close(c.closed) })
81+
return nil
82+
}
83+
84+
func (*deadlinelessConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
85+
func (*deadlinelessConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
86+
func (*deadlinelessConn) SetDeadline(time.Time) error { return errors.New("deadline not supported") }
87+
func (*deadlinelessConn) SetReadDeadline(time.Time) error {
88+
return errors.New("deadline not supported")
89+
}
90+
func (*deadlinelessConn) SetWriteDeadline(time.Time) error {
91+
return errors.New("deadline not supported")
92+
}
93+
94+
// TestCloseUnblocksWhenSetReadDeadlineFails exercises the deadlock path where
95+
// SetReadDeadline cannot unblock the binlog reader (e.g. ssh tunnel) and KILL
96+
// also fails to reach the server (thread already reaped). Under the previous
97+
// behaviour close() parked indefinitely on wg.Wait.
98+
func TestCloseUnblocksWhenSetReadDeadlineFails(t *testing.T) {
99+
b := NewBinlogSyncer(BinlogSyncerConfig{ServerID: 1})
100+
101+
fake := newDeadlinelessConn()
102+
b.c = &client.Conn{Conn: packet.NewConn(fake)}
103+
b.running = true
104+
105+
// Mimic onStream's parked ReadPacket: block until the underlying conn is
106+
// closed, then honour ctx cancellation before signalling wg.Done.
107+
b.wg.Add(1)
108+
go func() {
109+
defer b.wg.Done()
110+
_, _ = b.c.ReadPacket()
111+
<-b.ctx.Done()
112+
}()
113+
114+
done := make(chan struct{})
115+
go func() {
116+
b.Close()
117+
close(done)
118+
}()
119+
120+
select {
121+
case <-done:
122+
case <-time.After(5 * time.Second):
123+
t.Fatal("Close hung when SetReadDeadline refused a deadline")
124+
}
125+
}

0 commit comments

Comments
 (0)