Skip to content

fix(adapter-pg): release the pool client at most once per transaction - #29954

Open
ianduvall wants to merge 3 commits into
prisma:v7from
ianduvall:ianduvall/fix-adapter-pg-double-release
Open

fix(adapter-pg): release the pool client at most once per transaction#29954
ianduvall wants to merge 3 commits into
prisma:v7from
ianduvall:ianduvall/fix-adapter-pg-double-release

Conversation

@ianduvall

@ianduvall ianduvall commented Aug 10, 2026

Copy link
Copy Markdown

Fixes the adapter side of #29952: @prisma/adapter-pg double-releases the pg pool client when the query engine settles an interactive transaction twice, which eventually kills the Node process with an uncatchable error.

Mechanism

When an interactive transaction timeout expires while COMMIT is awaiting the driver adapter, the engine's tx_timeout! select drops the commit future, but the napi-backed JS chain cannot be cancelled and eventually calls PgTransaction.commit()client.release(). The timeout arm then compensates with a rollback, producing a second chain: executeRaw("ROLLBACK") on the still-busy client, then PgTransaction.rollback()client.release() again. pg-pool's double-release guard is per-checkout, so when the pool has re-lent the client between the two releases, the second release lands silently on the new owner's fresh closure and pushes a busy client back into the idle set. The new owner's own release then throws Release called on client which has already been released to the pool inside pg's socket-data handler, rethrown on process.nextTick — uncatchable, killing the process.

The root cause (the engine dropping an uncancellable JS commit future and compensating while the first chain is still running) is in prisma-engines and is out of scope here; this PR hardens the adapter so a double settlement can no longer crash the process. App-visible behavior is unchanged: the racing transaction still fails with the engine's expired-transaction error.

Fix

PgTransaction settles at most once:

  • A second commit()/rollback() is a no-op — never a second client.release().
  • queryRaw/executeRaw after settlement reject with TransactionAlreadyClosed without dispatching SQL, so the compensation's ROLLBACK cannot land on a client the pool already re-lent. The normal usePhantomQuery: false flow (the engine sends COMMIT/ROLLBACK as executeRaw on the open transaction before settling) is unaffected and pinned by a test.
  • If settlement finds statements still in flight (the engine abandoned the transaction mid-operation), the client is released with an error so pg-pool destroys the connection instead of re-lending a busy one. In-flight detection is adapter-owned (a counter around performIO) rather than reaching into pg client internals.

Tests

The new PgTransaction tests stub pg.Client's connect/query/end (no database needed) and script the engine's settlement sequences directly, including the exact re-lend interleaving from the issue. All four regression tests fail on the unpatched adapter — the re-lend test reproduces the exact Release called on client which has already been released to the pool error — and pass with the fix. The pre-existing suite stays green.

Sibling adapter audit

Kept out of this PR to keep the fix reviewable:

  • adapter-neon has the verbatim same commit()/rollback()client.release() shape and the same hole; this fix applies to it directly.
  • adapter-mariadb has the same class of hole: unguarded double settlement double-end()s the pooled connection and can dispatch late SQL on a re-lent connection.
  • adapter-planetscale settles idempotently via a deferred, but still dispatches SQL after settlement.
  • adapter-libsql and adapter-better-sqlite3 double-invoke client.commit()/unlockParent() on double settlement — different, milder failure modes.
  • adapter-mssql (mutex plus the tedious transaction state machine), adapter-d1 (no-op settlement), and adapter-ppg (already guarded with a #finished flag) are not exposed to the pool-corruption scenario.

Summary by CodeRabbit

  • Bug Fixes

    • Improved PostgreSQL transaction cleanup when queries are still in progress.
    • Prevented clients from being released more than once or reused after transaction completion.
    • Queries issued after commit or rollback are now rejected without being sent to the database.
    • Ensured busy connections are safely destroyed when a transaction settles during an active query.
  • Tests

    • Added coverage for transaction settlement, client release, post-settlement queries, and in-flight operations.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f9d02601-0f48-42b0-9c02-b3e8db725ff0

📥 Commits

Reviewing files that changed from the base of the PR and between f0714a3 and 6abb9de.

📒 Files selected for processing (2)
  • packages/adapter-pg/src/__tests__/pg.test.ts
  • packages/adapter-pg/src/pg.ts

📝 Walkthrough

Walkthrough

PgTransaction now prevents post-settlement queries, tracks in-flight queries, and releases PostgreSQL clients exactly once. Commit and rollback share the settlement path. Tests cover duplicate settlement, re-lent clients, rejected queries, and destruction of busy connections.

Changes

PostgreSQL transaction settlement

Layer / File(s) Summary
Transaction execution and settlement
packages/adapter-pg/src/pg.ts
PgTransaction tracks settlement and in-flight queries. It rejects queries after settlement and releases the client once, using an error when queries remain active. Commit and rollback use the shared settlement path.
Settlement regression coverage
packages/adapter-pg/src/__tests__/pg.test.ts
Tests stub PostgreSQL clients and verify client release, duplicate settlement, protection of re-lent clients, post-settlement query rejection, and destruction of connections with pending queries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • prisma/prisma#29979: Both PRs modify PgQueryable.performIO and transaction query handling, but address different concerns.

Suggested labels: lgtm

Suggested reviewers: aqrln

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing multiple pool-client releases per PostgreSQL transaction.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CLAassistant

CLAassistant commented Aug 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

daltino

This comment was marked as resolved.

@wmadden-electric

Copy link
Copy Markdown
Contributor

Thanks for this — the mechanism write-up and the linked issue made it easy to follow.

One process ask before we review: could you retarget this at v7 rather than 7.9.x? We take external contributions against v7; 7.9.x is a maintenance branch we drive ourselves. You can change the base with the Edit button next to the PR title.

It should be a clean move — packages/adapter-pg/src/pg.ts and src/__tests__/pg.test.ts are identical on both branches right now, so nothing should need rebasing. Worth dropping the (7.9.x) from the title while you're there.

One thing you've probably noticed: CI hasn't run here, and that's on us rather than on your change. Workflow runs on this branch currently fail to start before they reach the approval step. The fix for that landed on v7 this morning in #29951, so moving across gets you working CI too.

Your DCO sign-off and CLA are both already in order, so the retarget is the only thing outstanding.

@ianduvall
ianduvall force-pushed the ianduvall/fix-adapter-pg-double-release branch from 46d2b35 to 3d22d7a Compare August 11, 2026 16:32
@ianduvall ianduvall changed the title fix(adapter-pg): release the pool client at most once per transaction (7.9.x) fix(adapter-pg): release the pool client at most once per transaction Aug 11, 2026
@ianduvall
ianduvall changed the base branch from 7.9.x to v7 August 11, 2026 16:32
@ianduvall

Copy link
Copy Markdown
Author

@wmadden-electric done, thanks!

When an interactive transaction timeout expires while COMMIT is in
flight, the query engine settles the transaction twice: the abandoned
commit chain and the compensating rollback both reach the adapter. The
second client.release() lands on a client pg-pool has already re-lent,
silently corrupting the pool accounting until the new owner releases and
pg throws an uncatchable double-release error inside its socket-data
handler, killing the process.

PgTransaction now settles at most once: the second commit/rollback is a
no-op, queries after settlement reject with TransactionAlreadyClosed
without dispatching SQL, and a client with statements still in flight at
settlement is released with an error so the pool destroys the connection
instead of re-lending a busy one.

Fixes prisma#29952

Claude-Session: https://claude.ai/code/session_01U3QGpkQTcYEAGGn5PVy6WG
Signed-off-by: Ian Duvall <ian@omni.co>
@ianduvall
ianduvall force-pushed the ianduvall/fix-adapter-pg-double-release branch from 3d22d7a to f0714a3 Compare August 12, 2026 16:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/adapter-pg/src/__tests__/pg.test.ts`:
- Line 197: Update the five new test descriptions in the pg test suite to remove
the leading “should” wording, while preserving the rest of each description and
its intended behavior.
- Around line 173-195: Update the test setup around setup to ensure every newly
created pg.Pool is closed after each test, either by returning a teardown
function that invokes pool.end() and calling it from each test or by registering
pools for centralized afterEach cleanup. Preserve the existing adapter and
release-tracking behavior while preventing pools from remaining active after
tests complete.

In `@packages/adapter-pg/src/pg.ts`:
- Around line 172-185: Update settle() so the client release logic always
executes even when this.cleanup?.() throws: wrap cleanup invocation in a
try/finally and keep both in-flight and normal release paths inside the finally
block.
- Around line 147-161: Update the performIO override return type from
pg.QueryArrayResult<any> to pg.QueryArrayResult<unknown[]> while preserving the
existing transaction and in-flight query behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e033b821-4f19-4f90-80be-86e5ab268c93

📥 Commits

Reviewing files that changed from the base of the PR and between ce5a34c and f0714a3.

📒 Files selected for processing (2)
  • packages/adapter-pg/src/__tests__/pg.test.ts
  • packages/adapter-pg/src/pg.ts

Comment thread packages/adapter-pg/src/__tests__/pg.test.ts
Comment thread packages/adapter-pg/src/__tests__/pg.test.ts
Comment thread packages/adapter-pg/src/pg.ts
Comment thread packages/adapter-pg/src/pg.ts
Each PgTransaction settlement test creates its own pg.Pool through
setup() but never closed it, leaking live pools past the point where
afterEach restores the mocked pg.Client methods. Track the pools setup()
creates and end them in afterEach before restoring mocks.

Claude-Session: https://claude.ai/code/session_013ZRubEFkLdS4givbjSKXBJ
Signed-off-by: Ian Duvall <ian@omni.co>
settle() exists to guarantee an at-most-once release of the pooled
client, but it invoked the cleanup callback before the release, so a
throwing callback could skip the release entirely. Wrap the callback in
try/finally so the release is unconditional.

Claude-Session: https://claude.ai/code/session_013ZRubEFkLdS4givbjSKXBJ
Signed-off-by: Ian Duvall <ian@omni.co>
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.

4 participants