Skip to content

Commit 6091372

Browse files
committed
Update PgBouncer guidance and expand integration coverage
Document prepared statement support in PgBouncer 1.21+ and test all compatible query execution modes, including parameterized queries, transactions, Exec, and batches. Also enable pgbouncer tests in CI.
1 parent 140be86 commit 6091372

8 files changed

Lines changed: 223 additions & 44 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,33 @@ jobs:
128128
PGX_SSL_PASSWORD: ${{ matrix.pgx-ssl-password }}
129129
PGX_TEST_TLS_CLIENT_CONN_STRING: ${{ matrix.pgx-test-tls-client-conn-string }}
130130

131+
test-pgbouncer:
132+
name: Test PgBouncer with PostgreSQL 18
133+
runs-on: ubuntu-22.04
134+
135+
steps:
136+
- name: Check out code into the Go module directory
137+
uses: actions/checkout@v5
138+
139+
- name: Set up Go 1.26
140+
uses: actions/setup-go@v6
141+
with:
142+
go-version: "1.26"
143+
144+
- name: Set up PostgreSQL 18
145+
run: ci/setup_test.bash
146+
env:
147+
PGVERSION: "18"
148+
149+
- name: Set up PgBouncer
150+
run: ci/setup_pgbouncer.bash
151+
152+
- name: Test PgBouncer
153+
run: go test -parallel=1 -race -count=1 -run 'TestPgbouncer|TestConnContextCanceledCancelsRunningQueryOnServer' ./...
154+
env:
155+
PGX_TEST_DATABASE: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
156+
PGX_TEST_PGBOUNCER_CONN_STRING: "host=127.0.0.1 port=6432 user=pgx_md5 password=secret dbname=pgx_test"
157+
131158
test-windows:
132159
name: Test Windows
133160
runs-on: windows-latest

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ psql --no-psqlrc -f testsetup/postgresql_setup.sql
130130
### PgBouncer
131131

132132
There are tests specific for PgBouncer that will be executed if `PGX_TEST_PGBOUNCER_CONN_STRING` is set.
133+
The test PgBouncer must be version 1.21.0 or newer, use transaction pooling, and have `max_prepared_statements` set to a
134+
non-zero value. This ensures the tests cover PgBouncer's protocol-level named prepared statement support in addition to
135+
the pgx query modes that do not use named prepared statements.
133136

134137
### Optional Tests
135138

ci/setup_pgbouncer.bash

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
set -euxo pipefail
3+
4+
sudo apt-get install -y pgbouncer
5+
sudo systemctl stop pgbouncer
6+
7+
sudo install -o postgres -g postgres -m 0750 -d /etc/pgbouncer
8+
sudo install -o postgres -g postgres -m 0640 testsetup/pgbouncer.ini /etc/pgbouncer/pgbouncer.ini
9+
sudo install -o postgres -g postgres -m 0640 testsetup/pgbouncer-userlist.txt /etc/pgbouncer/userlist.txt
10+
sudo -u postgres pgbouncer -d /etc/pgbouncer/pgbouncer.ini
11+
12+
for _ in {1..30}; do
13+
if PGPASSWORD=secret psql "host=127.0.0.1 port=6432 user=pgx_md5 dbname=pgx_test" -c "select 1"; then
14+
exit 0
15+
fi
16+
sleep 1
17+
done
18+
19+
sudo cat /var/log/postgresql/pgbouncer.log || true
20+
exit 1

conn.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ type ConnConfig struct {
3636
DescriptionCacheCapacity int
3737

3838
// DefaultQueryExecMode controls the default mode for executing queries. By default pgx uses the extended protocol
39-
// and automatically prepares and caches prepared statements. However, this may be incompatible with proxies such as
40-
// PGBouncer. In this case it may be preferable to use [QueryExecModeExec] or [QueryExecModeSimpleProtocol]. The same
41-
// functionality can be controlled on a per query basis by passing a [QueryExecMode] as the first query argument.
39+
// and automatically prepares and caches prepared statements. This may be incompatible with proxies such as PgBouncer
40+
// unless they are configured to support protocol-level prepared statements. In an incompatible configuration it may
41+
// be preferable to use [QueryExecModeExec] or [QueryExecModeSimpleProtocol]. The same functionality can be controlled
42+
// on a per query basis by passing a [QueryExecMode] as the first query argument.
4243
DefaultQueryExecMode QueryExecMode
4344

4445
createdByParseConfig bool // Used to enforce created by ParseConfig rule.

doc.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,18 @@ implemented on top of [pgconn.PgConn]. The [Conn.PgConn] method can be used to a
210210
211211
PgBouncer
212212
213-
By default pgx automatically uses prepared statements. Prepared statements are incompatible with PgBouncer. This can be
214-
disabled by setting a different [QueryExecMode] in [ConnConfig.DefaultQueryExecMode].
213+
By default pgx automatically uses protocol-level named prepared statements. PgBouncer 1.21.0 and newer can use these
214+
prepared statements in transaction and statement pooling modes when its max_prepared_statements setting is greater than
215+
zero. In this configuration the default [QueryExecModeCacheStatement] mode may be used. See the PgBouncer documentation
216+
for limitations and configuration details: https://www.pgbouncer.org/config.html#max_prepared_statements.
217+
218+
When using an older PgBouncer version or when prepared statement support is disabled, set
219+
[ConnConfig.DefaultQueryExecMode] to [QueryExecModeExec]. [QueryExecModeCacheDescribe] and
220+
[QueryExecModeSimpleProtocol] are also compatible with PgBouncer. Prefer [QueryExecModeExec] over
221+
[QueryExecModeSimpleProtocol] whenever possible. Do not use [QueryExecModeDescribeExec] with transaction pooling because
222+
PgBouncer may assign a different server connection between the describe and execute steps.
223+
224+
SQL-level prepared statements created with PREPARE are not supported by PgBouncer in transaction pooling mode.
215225
*/
216226
package pgx
217227

pgbouncer_test.go

Lines changed: 134 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,169 @@ package pgx_test
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"os"
68
"testing"
9+
"time"
710

811
"github.com/jackc/pgx/v5"
9-
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
13+
"golang.org/x/sync/errgroup"
1114
)
1215

13-
func TestPgbouncerStatementCacheDescribe(t *testing.T) {
16+
func TestPgbouncerQueryExecModes(t *testing.T) {
1417
connString := os.Getenv("PGX_TEST_PGBOUNCER_CONN_STRING")
1518
if connString == "" {
1619
t.Skipf("Skipping due to missing environment variable %v", "PGX_TEST_PGBOUNCER_CONN_STRING")
1720
}
1821

19-
config := mustParseConfig(t, connString)
20-
config.DefaultQueryExecMode = pgx.QueryExecModeCacheDescribe
21-
config.DescriptionCacheCapacity = 1024
22+
tests := []struct {
23+
name string
24+
mode pgx.QueryExecMode
25+
statementCacheCapacity int
26+
descriptionCacheCapacity int
27+
}{
28+
{
29+
name: "cache statement",
30+
mode: pgx.QueryExecModeCacheStatement,
31+
statementCacheCapacity: 32,
32+
},
33+
{
34+
name: "cache describe",
35+
mode: pgx.QueryExecModeCacheDescribe,
36+
descriptionCacheCapacity: 32,
37+
},
38+
{
39+
name: "exec",
40+
mode: pgx.QueryExecModeExec,
41+
},
42+
{
43+
name: "simple protocol",
44+
mode: pgx.QueryExecModeSimpleProtocol,
45+
},
46+
}
2247

23-
testPgbouncer(t, config, 10, 100)
48+
for _, tt := range tests {
49+
t.Run(tt.name, func(t *testing.T) {
50+
config := mustParseConfig(t, connString)
51+
config.DefaultQueryExecMode = tt.mode
52+
config.StatementCacheCapacity = tt.statementCacheCapacity
53+
config.DescriptionCacheCapacity = tt.descriptionCacheCapacity
54+
55+
testPgbouncer(t, config, 10, 100)
56+
})
57+
}
2458
}
2559

26-
func TestPgbouncerSimpleProtocol(t *testing.T) {
60+
func TestPgbouncerStatementCacheEviction(t *testing.T) {
2761
connString := os.Getenv("PGX_TEST_PGBOUNCER_CONN_STRING")
2862
if connString == "" {
2963
t.Skipf("Skipping due to missing environment variable %v", "PGX_TEST_PGBOUNCER_CONN_STRING")
3064
}
3165

3266
config := mustParseConfig(t, connString)
33-
config.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
67+
config.DefaultQueryExecMode = pgx.QueryExecModeCacheStatement
68+
config.StatementCacheCapacity = 8
3469

35-
testPgbouncer(t, config, 10, 100)
70+
conn, err := pgx.ConnectConfig(t.Context(), config)
71+
require.NoError(t, err)
72+
defer conn.Close(t.Context())
73+
74+
for i := range 200 {
75+
sql := fmt.Sprintf("select $1::int4 /* statement cache eviction %d */", i)
76+
var n int32
77+
err := conn.QueryRow(t.Context(), sql, int32(i)).Scan(&n)
78+
require.NoError(t, err)
79+
require.EqualValues(t, i, n)
80+
}
3681
}
3782

3883
func testPgbouncer(t *testing.T, config *pgx.ConnConfig, workers, iterations int) {
39-
doneChan := make(chan struct{})
84+
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
85+
defer cancel()
86+
87+
eg, ctx := errgroup.WithContext(ctx)
4088

4189
for range workers {
42-
go func() {
43-
defer func() { doneChan <- struct{}{} }()
44-
conn, err := pgx.ConnectConfig(context.Background(), config)
45-
require.Nil(t, err)
46-
defer closeConn(t, conn)
47-
48-
for range iterations {
49-
var i32 int32
50-
var i64 int64
51-
var f32 float32
52-
var s string
53-
var s2 string
54-
err = conn.QueryRow(context.Background(), "select 1::int4, 2::int8, 3::float4, 'hi'::text").Scan(&i32, &i64, &f32, &s)
55-
require.NoError(t, err)
56-
assert.Equal(t, int32(1), i32)
57-
assert.Equal(t, int64(2), i64)
58-
assert.Equal(t, float32(3), f32)
59-
assert.Equal(t, "hi", s)
60-
61-
err = conn.QueryRow(context.Background(), "select 1::int8, 2::float4, 'bye'::text, 4::int4, 'whatever'::text").Scan(&i64, &f32, &s, &i32, &s2)
62-
require.NoError(t, err)
63-
assert.Equal(t, int64(1), i64)
64-
assert.Equal(t, float32(2), f32)
65-
assert.Equal(t, "bye", s)
66-
assert.Equal(t, int32(4), i32)
67-
assert.Equal(t, "whatever", s2)
90+
eg.Go(func() (err error) {
91+
conn, err := pgx.ConnectConfig(ctx, config)
92+
if err != nil {
93+
return err
6894
}
69-
}()
95+
defer func() {
96+
err = errors.Join(err, conn.Close(ctx))
97+
}()
98+
99+
return exercisePgbouncerConn(ctx, conn, iterations)
100+
})
70101
}
71102

72-
for range workers {
73-
<-doneChan
103+
require.NoError(t, eg.Wait())
104+
}
105+
106+
func exercisePgbouncerConn(ctx context.Context, conn *pgx.Conn, iterations int) error {
107+
for i := range iterations {
108+
var i32 int32
109+
var i64 int64
110+
var f32 float32
111+
var s string
112+
113+
err := conn.QueryRow(ctx, "select $1::int4, $2::int8, $3::float4, $4::text", int32(i), int64(i+1), float32(i+2), "hi").Scan(&i32, &i64, &f32, &s)
114+
if err != nil {
115+
return err
116+
}
117+
if i32 != int32(i) || i64 != int64(i+1) || f32 != float32(i+2) || s != "hi" {
118+
return fmt.Errorf("unexpected query result: %d, %d, %f, %q", i32, i64, f32, s)
119+
}
120+
}
121+
122+
commandTag, err := conn.Exec(ctx, "select $1::int4", int32(42))
123+
if err != nil {
124+
return err
74125
}
126+
if commandTag.String() != "SELECT 1" {
127+
return fmt.Errorf("unexpected command tag: %s", commandTag)
128+
}
129+
130+
tx, err := conn.Begin(ctx)
131+
if err != nil {
132+
return err
133+
}
134+
var txValue int32
135+
if err := tx.QueryRow(ctx, "select $1::int4", int32(43)).Scan(&txValue); err != nil {
136+
_ = tx.Rollback(ctx)
137+
return err
138+
}
139+
if txValue != 43 {
140+
_ = tx.Rollback(ctx)
141+
return fmt.Errorf("unexpected transaction query result: %d", txValue)
142+
}
143+
if err := tx.Commit(ctx); err != nil {
144+
return err
145+
}
146+
147+
batch := &pgx.Batch{}
148+
batch.Queue("select $1::int4", int32(44))
149+
batch.Queue("select $1::text", "batch")
150+
batchResults := conn.SendBatch(ctx, batch)
151+
152+
var batchInt int32
153+
if err := batchResults.QueryRow().Scan(&batchInt); err != nil {
154+
_ = batchResults.Close()
155+
return err
156+
}
157+
var batchText string
158+
if err := batchResults.QueryRow().Scan(&batchText); err != nil {
159+
_ = batchResults.Close()
160+
return err
161+
}
162+
if err := batchResults.Close(); err != nil {
163+
return err
164+
}
165+
if batchInt != 44 || batchText != "batch" {
166+
return fmt.Errorf("unexpected batch query results: %d, %q", batchInt, batchText)
167+
}
168+
169+
return nil
75170
}

testsetup/pgbouncer-userlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"pgx_md5" "secret"

testsetup/pgbouncer.ini

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[databases]
2+
pgx_test = host=127.0.0.1 port=5432 dbname=pgx_test user=pgx_md5 password=secret
3+
4+
[pgbouncer]
5+
listen_addr = 127.0.0.1
6+
listen_port = 6432
7+
unix_socket_dir = /var/run/postgresql
8+
9+
auth_type = md5
10+
auth_file = /etc/pgbouncer/userlist.txt
11+
admin_users = pgx_md5
12+
13+
pool_mode = transaction
14+
default_pool_size = 2
15+
max_client_conn = 100
16+
max_prepared_statements = 200
17+
18+
server_tls_sslmode = disable
19+
client_tls_sslmode = disable
20+
21+
logfile = /var/log/postgresql/pgbouncer.log
22+
pidfile = /var/run/postgresql/pgbouncer.pid

0 commit comments

Comments
 (0)