Skip to content

Commit 6d76f2d

Browse files
seveibarSeverin Ibarluzea
andauthored
feat: pgbouncer container support (#26)
Co-authored-by: Severin Ibarluzea <seve@seve-squid.local>
1 parent 66852ea commit 6d76f2d

File tree

6 files changed

+922
-735
lines changed

6 files changed

+922
-735
lines changed

src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const getTestPostgresDatabaseFactory = <
5858
const initialData: InitialWorkerData = {
5959
postgresVersion: options?.postgresVersion ?? "14",
6060
containerOptions: options?.container,
61+
pgbouncerOptions: options?.pgbouncer,
6162
}
6263

6364
const workerPromise = getWorker(initialData, options as any)
@@ -74,9 +75,18 @@ export const getTestPostgresDatabaseFactory = <
7475
connectionString: connectionDetailsFromWorker.connectionString,
7576
})
7677

78+
let pgbouncerPool: Pool | undefined
79+
if (connectionDetailsFromWorker.pgbouncerConnectionString) {
80+
pgbouncerPool = new Pool({
81+
connectionString:
82+
connectionDetailsFromWorker.pgbouncerConnectionString,
83+
})
84+
}
85+
7786
t.teardown(async () => {
7887
try {
7988
await pool.end()
89+
await pgbouncerPool?.end()
8090
} catch (error) {
8191
if (
8292
(error as Error).message.includes(
@@ -93,6 +103,7 @@ export const getTestPostgresDatabaseFactory = <
93103
return {
94104
...connectionDetailsFromWorker,
95105
pool,
106+
pgbouncerPool,
96107
}
97108
}
98109

src/internal-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
export interface InitialWorkerData {
99
postgresVersion: string
1010
containerOptions?: GetTestPostgresDatabaseFactoryOptions<any>["container"]
11+
pgbouncerOptions?: GetTestPostgresDatabaseFactoryOptions<any>["pgbouncer"]
1112
}
1213

1314
export type ConnectionDetailsFromWorker = Omit<ConnectionDetails, "pool">

src/public-types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { BindMode } from "testcontainers/build/types"
77
export interface ConnectionDetails {
88
connectionString: string
99
connectionStringDocker: string
10+
pgbouncerConnectionString?: string
11+
pgbouncerConnectionStringDocker?: string
1012
dockerNetworkId: string
1113

1214
host: string
@@ -16,6 +18,9 @@ export interface ConnectionDetails {
1618
database: string
1719

1820
pool: Pool
21+
22+
// TODO if pgbouncer is enabled, this is defined, otherwise undefined
23+
pgbouncerPool?: Pool
1924
}
2025

2126
export interface GetTestPostgresDatabaseFactoryOptions<
@@ -32,6 +37,16 @@ export interface GetTestPostgresDatabaseFactoryOptions<
3237
mode?: BindMode
3338
}[]
3439
}
40+
41+
/**
42+
* Pgbouncer container settings, disabled by default.
43+
*/
44+
pgbouncer?: {
45+
enabled: boolean
46+
version?: string
47+
poolMode?: "session" | "transaction" | "statement"
48+
}
49+
3550
/**
3651
* Test workers will be de-duped by this key. You probably don't need to set this.
3752
*/

src/tests/pgbouncer.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import test from "ava"
2+
import { QueryResult } from "pg"
3+
import { getTestPostgresDatabaseFactory } from "~/index"
4+
5+
test("pgbouncer", async (t) => {
6+
const getPostgres13 = getTestPostgresDatabaseFactory({
7+
postgresVersion: "13.5",
8+
pgbouncer: {
9+
enabled: true,
10+
version: "1.22.0",
11+
poolMode: "statement",
12+
},
13+
})
14+
15+
const postgres13 = await getPostgres13(t)
16+
17+
t.truthy(postgres13.pgbouncerConnectionString)
18+
const result = await postgres13.pool.query("SELECT 1 as result")
19+
20+
t.is(result.rows[0].result, 1)
21+
22+
// can't use a transaction with statement pool mode
23+
const err = await t.throwsAsync(
24+
postgres13.pgbouncerPool!.query(`BEGIN TRANSACTION`)
25+
)
26+
27+
t.truthy(
28+
err!
29+
.toString()
30+
.includes("transaction blocks not allowed in statement pooling mode")
31+
)
32+
})

src/worker.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,9 @@ export class Worker {
178178
const databaseName = getRandomDatabaseName()
179179

180180
// Create database
181-
const { postgresClient, container } = await this.startContainerPromise
181+
const { postgresClient, container, pgbouncerContainer } = await this
182+
.startContainerPromise
183+
182184
await postgresClient.query(`CREATE DATABASE ${databaseName};`)
183185

184186
const msg = message.reply({
@@ -246,16 +248,29 @@ export class Worker {
246248
}
247249

248250
private async getConnectionDetails(databaseName: string) {
249-
const { container, network } = await this.startContainerPromise
251+
const { container, network, pgbouncerContainer } = await this
252+
.startContainerPromise
250253
const externalDatabaseUrl = `postgresql://postgres:@${container.getHost()}:${container.getMappedPort(
251254
5432
252255
)}/${databaseName}`
253256

257+
let pgbouncerConnectionString, pgbouncerConnectionStringDocker
258+
if (pgbouncerContainer) {
259+
pgbouncerConnectionString = `postgresql://postgres:@${pgbouncerContainer.getHost()}:${pgbouncerContainer.getMappedPort(
260+
6432
261+
)}/${databaseName}`
262+
pgbouncerConnectionStringDocker = `postgresql://postgres:@${pgbouncerContainer
263+
.getName()
264+
.replace("/", "")}:5432/${databaseName}`
265+
}
266+
254267
return {
255268
connectionString: externalDatabaseUrl,
256269
connectionStringDocker: `postgresql://postgres:@${container
257270
.getName()
258271
.replace("/", "")}:5432/${databaseName}`,
272+
pgbouncerConnectionString,
273+
pgbouncerConnectionStringDocker,
259274
dockerNetworkId: network.getId(),
260275
host: container.getHost(),
261276
port: container.getMappedPort(5432),
@@ -300,13 +315,35 @@ export class Worker {
300315
)
301316
}
302317

318+
const connectionString = `postgresql://postgres:@${startedContainer.getHost()}:${startedContainer.getMappedPort(
319+
5432
320+
)}/postgres`
321+
322+
let startedPgbouncerContainer
323+
if (this.initialData.pgbouncerOptions?.enabled) {
324+
const pgbouncerContainer = new GenericContainer("edoburu/pgbouncer")
325+
.withExposedPorts(6432)
326+
.withName(getRandomDatabaseName())
327+
.withEnvironment({
328+
DB_HOST: startedContainer.getName().replace("/", ""),
329+
DB_USER: "postgres",
330+
DB_NAME: "*",
331+
POOL_MODE:
332+
this.initialData.pgbouncerOptions?.poolMode ?? "transaction",
333+
LISTEN_PORT: "6432",
334+
AUTH_TYPE: "trust",
335+
})
336+
.withStartupTimeout(120_000)
337+
.withNetwork(network)
338+
startedPgbouncerContainer = await pgbouncerContainer.start()
339+
}
340+
303341
return {
304342
container: startedContainer,
343+
pgbouncerContainer: startedPgbouncerContainer,
305344
network,
306345
postgresClient: new pg.Pool({
307-
connectionString: `postgresql://postgres:@${startedContainer.getHost()}:${startedContainer.getMappedPort(
308-
5432
309-
)}/postgres`,
346+
connectionString,
310347
}),
311348
}
312349
}

0 commit comments

Comments
 (0)