Skip to content

Commit d2f32bb

Browse files
committed
review
1 parent 798d83b commit d2f32bb

4 files changed

Lines changed: 68 additions & 64 deletions

File tree

flow/connectors/mysql/cdc.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/PeerDB-io/peerdb/flow/shared"
3030
"github.com/PeerDB-io/peerdb/flow/shared/datatypes"
3131
"github.com/PeerDB-io/peerdb/flow/shared/exceptions"
32+
"github.com/PeerDB-io/peerdb/flow/shared/timeout"
3233
"github.com/PeerDB-io/peerdb/flow/shared/types"
3334
)
3435

@@ -324,18 +325,11 @@ func (c *MySqlConnector) startCdcStreamingGtid(
324325
// for ssh tunnel, and killing a connection that has already been reaped by the server
325326
// but is not propagated to the client). This can lead to syncer.Close() stuck indefinitely.
326327
// TODO: better to fix properly upstream
327-
func (c *MySqlConnector) closeSyncerWithTimeout(syncer *replication.BinlogSyncer, timeout time.Duration) {
328-
done := make(chan struct{})
329-
go func() {
330-
syncer.Close()
331-
close(done)
332-
}()
333-
select {
334-
case <-done:
335-
case <-time.After(timeout):
336-
c.logger.Error("[mysql] syncer.Close hung, force-closing SSH tunnel to unblock")
328+
func (c *MySqlConnector) closeSyncer(syncer *replication.BinlogSyncer, deadline time.Duration) {
329+
timeout.CloseWithTimeout(syncer.Close, deadline, func() {
330+
c.logger.Error("[mysql] syncer.Close hung, force-closing SSH tunnel")
337331
_ = c.ssh.Close()
338-
}
332+
})
339333
}
340334

341335
func (c *MySqlConnector) ReplPing(context.Context) error {
@@ -373,7 +367,7 @@ func (c *MySqlConnector) PullRecords(
373367
if err != nil {
374368
return err
375369
}
376-
defer c.closeSyncerWithTimeout(syncer, 10*time.Second)
370+
defer c.closeSyncer(syncer, 10*time.Second)
377371

378372
c.logger.Info("[mysql] PullRecords started streaming")
379373

flow/connectors/mysql/ssh_keepalive_test.go

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ import (
2121
)
2222

2323
const (
24-
toxiproxyDownProxyPort = 42001
25-
toxiproxyLatencyProxyPort = 42002
26-
toxiproxyResetProxyPort = 42003
27-
toxiproxyCDCHangProxyPort = 42004
28-
toxiproxyCDCCloseHangProxyPort = 42005
29-
toxiproxyCloseSyncerWithTimeoutPort = 42006
24+
toxiproxyDownProxyPort = 42001
25+
toxiproxyLatencyProxyPort = 42002
26+
toxiproxyResetProxyPort = 42003
27+
toxiproxyCDCHangProxyPort = 42004
28+
toxiproxyCDCCloseHangProxyPort = 42005
3029
)
3130

3231
func setupMySQLConnectorWithProxy(ctx context.Context, t *testing.T, proxyName string, proxyPort int,
@@ -271,49 +270,3 @@ func TestMySQLSSHKeepaliveCDCCloseHang(t *testing.T) {
271270
t.Fatal("PullRecords did not return after SSH keepalive closed the connection")
272271
}
273272
}
274-
275-
func TestMySQLCloseSyncerWithTimeout(t *testing.T) {
276-
t.Parallel()
277-
if os.Getenv("CI_MYSQL_VERSION") == "maria" {
278-
t.Skip("Skipping for MariaDB")
279-
}
280-
281-
ctx := t.Context()
282-
connector, sshProxy := setupMySQLConnectorWithProxy(
283-
ctx, t, "my-close-syncer-timeout-test", toxiproxyCloseSyncerWithTimeoutPort)
284-
defer connector.Close()
285-
286-
pos, err := connector.GetMasterPos(ctx)
287-
require.NoError(t, err)
288-
syncer, _, _, _, err := connector.startCdcStreamingFilePos(ctx, pos)
289-
require.NoError(t, err)
290-
291-
// Let CDC streaming establish before blocking the tunnel.
292-
time.Sleep(2 * time.Second)
293-
294-
// Black-hole the SSH transport so syncer.Close() hangs
295-
_, err = sshProxy.AddToxic("latency", "latency", "", 1.0, toxiproxy.Attributes{
296-
"latency": 120000,
297-
})
298-
require.NoError(t, err)
299-
300-
// Must be < SSHKeepaliveInterval so the timing bounds below prove the syncCloseTimeout
301-
// (not SSH keepalive's own force-close) is what unblocked syncer.Close.
302-
syncerCloseTimeout := 2 * time.Second
303-
require.Less(t, syncerCloseTimeout, utils.SSHKeepaliveInterval)
304-
done := make(chan struct{})
305-
start := time.Now()
306-
go func() {
307-
connector.closeSyncerWithTimeout(syncer, syncerCloseTimeout)
308-
close(done)
309-
}()
310-
311-
select {
312-
case <-done:
313-
elapsed := time.Since(start)
314-
require.GreaterOrEqual(t, elapsed, syncerCloseTimeout)
315-
require.Less(t, elapsed, syncerCloseTimeout+time.Second)
316-
case <-time.After(10 * time.Second):
317-
t.Fatal("closeSyncerWithTimeout did not return on timeout")
318-
}
319-
}

flow/shared/timeout/close.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package timeout
2+
3+
import "time"
4+
5+
// CloseWithTimeout runs closer in a goroutine and waits up to timeout for it
6+
// to return. If it doesn't, onTimeout is invoked and CloseWithTimeout returns
7+
// without waiting further.
8+
func CloseWithTimeout(closer func(), timeout time.Duration, onTimeout func()) {
9+
done := make(chan struct{})
10+
go func() {
11+
closer()
12+
close(done)
13+
}()
14+
select {
15+
case <-done:
16+
case <-time.After(timeout):
17+
onTimeout()
18+
}
19+
}

flow/shared/timeout/close_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package timeout
2+
3+
import (
4+
"sync/atomic"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestCloseWithTimeout_FastClose(t *testing.T) {
12+
var onTimeoutCalls atomic.Int32
13+
CloseWithTimeout(
14+
func() {}, // returns immediately
15+
time.Second,
16+
func() { onTimeoutCalls.Add(1) },
17+
)
18+
require.Zero(t, onTimeoutCalls.Load(), "onTimeout should not fire when closer returns quickly")
19+
}
20+
21+
func TestCloseWithTimeout_ClosureHangs(t *testing.T) {
22+
var onTimeoutCalls atomic.Int32
23+
release := make(chan struct{})
24+
defer close(release)
25+
26+
timeout := 50 * time.Millisecond
27+
start := time.Now()
28+
CloseWithTimeout(
29+
func() { <-release }, // blocks
30+
timeout,
31+
func() { onTimeoutCalls.Add(1) },
32+
)
33+
elapsed := time.Since(start)
34+
35+
require.Equal(t, int32(1), onTimeoutCalls.Load())
36+
require.GreaterOrEqual(t, elapsed, timeout)
37+
require.Less(t, elapsed, timeout+time.Second)
38+
}

0 commit comments

Comments
 (0)