Skip to content

Prevent reserves from stranding after server-side termination - #1229

Open
realies wants to merge 1 commit into
porsager:masterfrom
realies:fix-reserve-strand
Open

realies wants to merge 1 commit into
porsager:masterfrom
realies:fix-reserve-strand

Conversation

@realies

@realies realies commented Sep 26, 2026

Copy link
Copy Markdown

A queued reserve() can wait forever when the server terminates a connection. The close handler removes the waiter from the pool queue and passes it into reconnect. Connection startup discards that reserve marker, expecting the pool queue to still contain the waiter, so nothing resolves it.

Keep reserve waiters in the pool queue whenever a connection attempt starts, including reconnects. The normal open handler can then hand out the connection. Also clear the closed session's query, results, and pending error: otherwise a fatal error from an interrupted query can reject a new reserve, and the pool hands the reconnected slot to that already-rejected waiter. A waiter rejected by a failed connection attempt removes itself from the queue, so the pool neither keeps reconnecting for it nor hands a later connection to it.

Adds two tests using the existing t() harness and a real server: terminate a reserved backend while another reserve is queued, and terminate a reserved backend while a query is sleeping before reserving again. Both fail before the patch and pass afterward.

Validation:

  • PostgreSQL 16: npm test passes, 266 tests each for ESM, CommonJS, and Deno.
  • PostgreSQL 18: all 266 ESM and 266 CommonJS tests pass; both new Deno regressions pass. The full Deno suite does not complete on PostgreSQL 18 for a reason unrelated to this change; it stops the same way without it.
  • ESLint passes.

Fixes #1195

Keep reserve waiters queued when a connection attempt starts, including reconnects after a server-side close. Clear the closed session query state so a stale fatal error cannot reject a new reserve.

Add real-server regressions for queued reserves and terminated in-flight queries.
colll78 added a commit to Anastasia-Labs/midgard that referenced this pull request Sep 29, 2026
postgres.js 3.4.9 can lose a queued reserve() request. When a pooled
connection closes or reconnects while requests wait for a connection, the pool
hands the next waiter to the reconnecting connection and drops it once that
connection is ready, so the request never settles (porsager/postgres#1195). A
waiter that fails with CONNECT_TIMEOUT also stays queued and later takes a
connection that is never returned.

@effect/sql-pg reserves every transaction's connection this way, inside an
acquisition that cannot be interrupted, so a timeout around the transaction
does not help. In the node this can strand the history-lease renewal: the owner
schedules no further renewal, the lease expires, and shutdown waits on the
stuck renewal. It is what hung the saturated batch-pool latency test in 2 of 11
local runs. Holding one batch connection's handshake past the 10 s connect
timeout reproduces it every time.

No released version contains the fix; 3.4.9 is the latest. This patch is the
source change of the open upstream PR porsager/postgres#1229, applied to the
ESM and CommonJS builds. Every reserve waiter stays in the pool's queue,
including one handed to a reconnect, a rejected waiter leaves the queue, and a
closed connection clears its per-query state. With the patch, the
connection-delay case passes in about 13 s. Drop the patch once a release
includes the fix.

pnpm also rewrites the lockfile's overrides in package.json order.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reserve() permanently stranded when a pooled connection is terminated server-side while the reserve is queued (3.4.9)

1 participant