Skip to content

Commit ed246d2

Browse files
authored
fix: leak of request into pools (#1206)
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
1 parent 81fb5fa commit ed246d2

6 files changed

Lines changed: 67 additions & 11 deletions

File tree

src/internal/database/pg-connection.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { vi } from 'vitest'
88
import {
99
getPgCancelConnectionTarget,
1010
PgPoolExecutor,
11+
PgPoolManager,
1112
PgPoolStrategy,
1213
PgTenantConnection,
1314
PgTransaction,
1415
} from './pg-connection'
16+
import type { TenantConnectionOptions } from './pool'
1517

1618
class TestablePgPoolStrategy extends PgPoolStrategy {
1719
getCurrentPoolForTest(): Pool {
@@ -24,8 +26,8 @@ class TestablePgPoolStrategy extends PgPoolStrategy {
2426
}
2527

2628
function createPoolStrategySettings(
27-
overrides: Partial<ConstructorParameters<typeof PgPoolStrategy>[0]> = {}
28-
): ConstructorParameters<typeof PgPoolStrategy>[0] {
29+
overrides: Partial<TenantConnectionOptions> = {}
30+
): TenantConnectionOptions {
2931
return {
3032
tenantId: 'pg-pool-strategy-test',
3133
dbUrl: 'postgres://postgres:postgres@localhost:5432/postgres',
@@ -741,6 +743,38 @@ describe('PgTenantConnection', () => {
741743
})
742744
})
743745

746+
describe('PgPoolManager', () => {
747+
it('caches strategies without retaining request-scoped options', async () => {
748+
const manager = new PgPoolManager()
749+
const tenantId = 'pg-pool-manager-prune-test'
750+
const request = { operation: { type: 'upload' } }
751+
752+
const strategy = manager.getPool(
753+
createPoolStrategySettings({
754+
tenantId,
755+
headers: { authorization: 'Bearer secret' },
756+
method: 'POST',
757+
path: '/object/bucket/key',
758+
operation: () => request.operation.type,
759+
})
760+
)
761+
762+
try {
763+
const retained = (strategy as unknown as { options: Record<string, unknown> }).options
764+
expect(Object.keys(retained).sort()).toEqual([
765+
'clusterSize',
766+
'dbUrl',
767+
'isExternalPool',
768+
'maxConnections',
769+
'numWorkers',
770+
'tenantId',
771+
])
772+
} finally {
773+
await manager.destroy(tenantId)
774+
}
775+
})
776+
})
777+
744778
describe('PgPoolStrategy', () => {
745779
it('logs idle pg pool errors without rethrowing them', async () => {
746780
const strategy = new TestablePgPoolStrategy(createPoolStrategySettings())

src/internal/database/pg-connection.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
PoolManager,
1010
PoolRebalanceOptions,
1111
PoolStats,
12+
PoolStrategySettings,
1213
searchPath,
1314
TenantConnectionOptions,
1415
} from './pool'
@@ -95,7 +96,7 @@ export class PgPoolStrategy {
9596
protected pool?: Pool
9697
protected tlsSession?: TlsSessionSlot
9798

98-
constructor(protected readonly options: TenantConnectionOptions) {}
99+
constructor(protected readonly options: PoolStrategySettings) {}
99100

100101
acquire(): PgPoolExecutor {
101102
return new PgPoolExecutor(this.getPool())
@@ -303,7 +304,7 @@ function pulsePgPoolQueue(pool: Pool): void {
303304
}
304305

305306
export class PgPoolManager extends PoolManager<PgPoolStrategy> {
306-
protected newPool(settings: TenantConnectionOptions): PgPoolStrategy {
307+
protected newPool(settings: PoolStrategySettings): PgPoolStrategy {
307308
return new PgPoolStrategy(settings)
308309
}
309310
}

src/internal/database/pool.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export interface TenantConnectionOptions {
4040
operation?: () => string | undefined
4141
}
4242

43+
// Pool cache entries are long-lived:
44+
// * minimum TENANT_POOL_CACHE_TTL_MS
45+
// * refreshed on access
46+
// Strategies must retain only pool settings and can't capture the request
47+
// that created them (headers, the operation closure over the whole Fastify request).
48+
export type PoolStrategySettings = Pick<
49+
TenantConnectionOptions,
50+
'tenantId' | 'dbUrl' | 'isExternalPool' | 'maxConnections' | 'clusterSize' | 'numWorkers'
51+
>
52+
4353
export interface User {
4454
jwt: string
4555
payload: { role?: string } & JWTPayload
@@ -263,7 +273,14 @@ export abstract class PoolManager<TPool extends PoolStrategy = PoolStrategy> {
263273
return existingPool as TPool
264274
}
265275

266-
const newPool = this.newPool({ ...settings, numWorkers: this.numWorkers })
276+
const newPool = this.newPool({
277+
tenantId: settings.tenantId,
278+
dbUrl: settings.dbUrl,
279+
isExternalPool: settings.isExternalPool,
280+
maxConnections: settings.maxConnections,
281+
clusterSize: settings.clusterSize,
282+
numWorkers: this.numWorkers,
283+
})
267284

268285
tenantPools.set(settings.tenantId, newPool)
269286
return newPool
@@ -296,5 +313,5 @@ export abstract class PoolManager<TPool extends PoolStrategy = PoolStrategy> {
296313
return Promise.allSettled(promises)
297314
}
298315

299-
protected abstract newPool(settings: TenantConnectionOptions): TPool
316+
protected abstract newPool(settings: PoolStrategySettings): TPool
300317
}

src/test/pg-connection.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import {
44
PgPoolStrategy,
55
PgTenantConnection,
66
} from '@internal/database'
7+
import type { TenantConnectionOptions } from '@internal/database/pool'
78
import { getConfig } from '../config'
89

910
const { databaseURL, databasePoolURL, tenantId } = getConfig()
1011

1112
describe('Pg database foundation', () => {
1213
function createConnectionSettings(
1314
superUser: Awaited<ReturnType<typeof getServiceKeyUser>>,
14-
overrides?: Partial<ConstructorParameters<typeof PgPoolStrategy>[0]>
15-
): ConstructorParameters<typeof PgPoolStrategy>[0] {
15+
overrides?: Partial<TenantConnectionOptions>
16+
): TenantConnectionOptions {
1617
return {
1718
tenantId,
1819
isExternalPool: true,

src/test/storage-pg-db.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
PgTransaction,
88
} from '@internal/database'
99
import { runMigrationsOnTenant } from '@internal/database/migrations'
10+
import type { TenantConnectionOptions } from '@internal/database/pool'
1011
import { logger, logSchema } from '@internal/monitoring'
1112
import { dbQueryPerformance } from '@internal/monitoring/metrics'
1213
import { StoragePgDB } from '@storage/database'
@@ -22,7 +23,7 @@ describe('StoragePgDB bucket metadata', () => {
2223
let db: StoragePgDB
2324
let runId: string
2425
let superUser: Awaited<ReturnType<typeof getServiceKeyUser>>
25-
let connectionSettings: ConstructorParameters<typeof PgPoolStrategy>[0]
26+
let connectionSettings: TenantConnectionOptions
2627

2728
beforeAll(async () => {
2829
await runMigrationsOnTenant({

src/test/vector-pg-store.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
PgTransaction,
66
} from '@internal/database'
77
import { runMigrationsOnTenant } from '@internal/database/migrations'
8+
import type { TenantConnectionOptions } from '@internal/database/pool'
89
import { logger, logSchema } from '@internal/monitoring'
910
import { PgVectorMetadataDB } from '@storage/protocols/vector'
1011
import { DatabaseError, type PoolClient } from 'pg'
@@ -25,14 +26,15 @@ describe('PgVectorMetadataDB', () => {
2526
})
2627

2728
const superUser = await getServiceKeyUser(tenantId)
28-
pool = new PgPoolStrategy({
29+
const connectionSettings: TenantConnectionOptions = {
2930
tenantId,
3031
dbUrl: databaseURL!,
3132
isExternalPool: false,
3233
maxConnections: 2,
3334
user: superUser,
3435
superUser,
35-
})
36+
}
37+
pool = new PgPoolStrategy(connectionSettings)
3638
db = new PgVectorMetadataDB(pool.acquire())
3739
})
3840

0 commit comments

Comments
 (0)