Files
orca/cloud/apps/relay/src/postgres-session-end-release.test.ts
T
Jinwoo Hong 8f117e19b1 fix(relay): destroy a pooled client whose session the server ended (#29573)
* fix(relay): destroy a pooled client whose session the server ended, and mark work that never ran

pg keeps calling a client queryable until its socket's end arrives, so a client released after
57P01 (pg_terminate_backend, shutdown) or a dropped socket went back to the pool and failed the
next caller's statement with 'Connection terminated unexpectedly'. Such clients are now released
with the error, so pg-pool discards them.

Also marks failures where no statement of the operation reached the server (acquire, a failed
BEGIN, a client dead at checkout) so a caller can tell work safe to re-run.

* fix(relay): skip ROLLBACK on an ended session, drop unused nothing-ran marking

Adds 57P04/57P05 to the session-ended codes and covers 08xxx, a dropped
socket, an ordinary 23505, and a real pg_terminate_backend.

Claude-Session: f2fe8cb5
2026-10-16 11:46:22 -07:00

64 lines
2.6 KiB
TypeScript

import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { PostgresDatabase } from './database.js'
// One client whose statements after BEGIN fail as given; records each statement and release.
function failingPool(failure: () => Error) {
const releases: unknown[] = []
const statements: string[] = []
const client = Object.assign(new EventEmitter(), {
query: async (sql: string) => {
statements.push(sql)
if (sql === 'BEGIN') return { rows: [], rowCount: null }
throw failure()
},
release: (error?: unknown) => void releases.push(error)
})
const pool = { totalCount: 1, idleCount: 0, waitingCount: 0, connect: async () => client }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: PostgresDatabase and its pool pressure read only connect, these counts and the optional options.
return { database: new PostgresDatabase(pool as never), releases, statements }
}
const withCode = (code: string) => () => Object.assign(new Error('server error'), { code })
describe('PostgreSQL session end', () => {
beforeEach(() => {
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
})
afterEach(() => {
vi.restoreAllMocks()
})
it.each([
['57P01', withCode('57P01')],
['57P05', withCode('57P05')],
['08006', withCode('08006')],
['Connection terminated', () => new Error('Connection terminated unexpectedly')]
])('destroys a client after %s, so no caller inherits it', async (_name, failure) => {
const query = failingPool(failure)
await expect(query.database.query('SELECT 1')).rejects.toThrow()
expect(query.releases).toEqual([expect.any(Error)])
const transaction = failingPool(failure)
await expect(
transaction.database.transaction(async (tx) => await tx.query('SELECT 1'))
).rejects.toThrow()
expect(transaction.releases).toEqual([expect.any(Error)])
expect(transaction.statements).not.toContain('ROLLBACK')
})
it('returns a client to the pool after an ordinary statement error', async () => {
const query = failingPool(withCode('23505'))
await expect(query.database.query('SELECT 1')).rejects.toMatchObject({ code: '23505' })
expect(query.releases).toEqual([undefined])
const transaction = failingPool(withCode('23505'))
await expect(
transaction.database.transaction(async (tx) => await tx.query('SELECT 1'))
).rejects.toMatchObject({ code: '23505' })
expect(transaction.releases).toEqual([undefined])
expect(transaction.statements).toContain('ROLLBACK')
})
})