Skip to content

Commit 6e7a097

Browse files
test: add delay proxy for ping test
1 parent c1c74da commit 6e7a097

2 files changed

Lines changed: 150 additions & 5 deletions

File tree

pgxpool/helper_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package pgxpool_test
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net"
7+
"path/filepath"
8+
"strconv"
9+
"strings"
10+
"sync"
11+
"sync/atomic"
12+
"testing"
13+
"time"
14+
15+
"github.com/stretchr/testify/require"
16+
17+
"github.com/jackc/pgx/v5/pgxpool"
18+
)
19+
20+
// delayProxy is a TCP proxy that introduces a configurable delay on reads from the database.
21+
type delayProxy struct {
22+
listener net.Listener
23+
targetAddr string
24+
targetNetwork string
25+
proxyConfig *pgxpool.Config
26+
readDelay time.Duration
27+
wg sync.WaitGroup
28+
closed atomic.Bool
29+
}
30+
31+
func newDelayProxy(t *testing.T, targetConnString string, readDelay time.Duration) *delayProxy {
32+
t.Helper()
33+
34+
config, err := pgxpool.ParseConfig(targetConnString)
35+
require.NoError(t, err)
36+
37+
listener, err := net.Listen("tcp", "127.0.0.1:0")
38+
require.NoError(t, err)
39+
40+
// Determine target network and address
41+
var targetNetwork, targetAddr string
42+
if strings.HasPrefix(config.ConnConfig.Host, "/") {
43+
targetNetwork = "unix"
44+
targetAddr = filepath.Join(config.ConnConfig.Host, fmt.Sprintf(".s.PGSQL.%d", config.ConnConfig.Port))
45+
} else {
46+
targetNetwork = "tcp"
47+
targetAddr = net.JoinHostPort(config.ConnConfig.Host, strconv.Itoa(int(config.ConnConfig.Port)))
48+
}
49+
50+
// Create proxy config with listener address
51+
proxyAddr := listener.Addr().(*net.TCPAddr)
52+
config.ConnConfig.Host = proxyAddr.IP.String()
53+
config.ConnConfig.Port = uint16(proxyAddr.Port)
54+
55+
p := &delayProxy{
56+
listener: listener,
57+
targetAddr: targetAddr,
58+
targetNetwork: targetNetwork,
59+
proxyConfig: config,
60+
readDelay: readDelay,
61+
}
62+
63+
p.wg.Add(1)
64+
go p.run()
65+
66+
t.Cleanup(func() { _ = p.Close() })
67+
68+
return p
69+
}
70+
71+
func (p *delayProxy) run() {
72+
defer p.wg.Done()
73+
74+
for {
75+
clientConn, err := p.listener.Accept()
76+
if err != nil {
77+
if p.closed.Load() {
78+
return
79+
}
80+
continue
81+
}
82+
83+
p.wg.Add(1)
84+
go p.handleConn(clientConn)
85+
}
86+
}
87+
88+
func (p *delayProxy) handleConn(clientConn net.Conn) {
89+
defer p.wg.Done()
90+
defer clientConn.Close()
91+
92+
serverConn, err := net.Dial(p.targetNetwork, p.targetAddr)
93+
if err != nil {
94+
return
95+
}
96+
defer serverConn.Close()
97+
98+
var wg sync.WaitGroup
99+
wg.Add(2)
100+
// Client -> Server (no delay)
101+
go func() {
102+
defer wg.Done()
103+
_, _ = io.Copy(serverConn, clientConn)
104+
if tcpConn, ok := serverConn.(*net.TCPConn); ok {
105+
_ = tcpConn.CloseWrite()
106+
}
107+
}()
108+
109+
// Server -> Client (with delay)
110+
go func() {
111+
defer wg.Done()
112+
delayedReader := &delayedReader{r: serverConn, delay: p.readDelay}
113+
_, _ = io.Copy(clientConn, delayedReader)
114+
if tcpConn, ok := clientConn.(*net.TCPConn); ok {
115+
_ = tcpConn.CloseWrite()
116+
}
117+
}()
118+
119+
wg.Wait()
120+
}
121+
122+
type delayedReader struct {
123+
r io.Reader
124+
delay time.Duration
125+
}
126+
127+
func (dr *delayedReader) Read(b []byte) (int, error) {
128+
if dr.delay > 0 {
129+
time.Sleep(dr.delay)
130+
}
131+
return dr.r.Read(b)
132+
}
133+
134+
func (p *delayProxy) Config() *pgxpool.Config {
135+
return p.proxyConfig.Copy()
136+
}
137+
138+
func (p *delayProxy) Close() error {
139+
if !p.closed.CompareAndSwap(false, true) {
140+
return nil
141+
}
142+
143+
err := p.listener.Close()
144+
p.wg.Wait()
145+
return err
146+
}

pgxpool/pool_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,21 +1288,20 @@ func TestPoolAcquirePingTimeout(t *testing.T) {
12881288
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
12891289
defer cancel()
12901290

1291-
config, err := pgxpool.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
1292-
require.NoError(t, err)
1291+
delayProxy := newDelayProxy(t, os.Getenv("PGX_TEST_DATABASE"), 500*time.Millisecond)
12931292

1294-
// Set a very short ping timeout to force timeout during ping and destruction of the connection
1295-
config.PingTimeout = 1 * time.Nanosecond
1293+
config := delayProxy.Config()
1294+
config.PingTimeout = 200 * time.Millisecond
12961295

12971296
var conID *uint32
12981297
// Only ping the connection with the original PID to force creation of a new connection
12991298
config.ShouldPing = func(_ context.Context, params pgxpool.ShouldPingParams) bool {
13001299
if conID != nil && params.Conn.PgConn().PID() == *conID {
13011300
return true
13021301
}
1303-
13041302
return false
13051303
}
1304+
13061305
// Limit to a single connection to ensure the same connection is reused
13071306
config.MinConns = 1
13081307
config.MaxConns = 1

0 commit comments

Comments
 (0)