Fix extended-query isolation for pooled postgres.js clients in pglite-socket#977
Open
sorenbs wants to merge 1 commit intoelectric-sql:mainfrom
Open
Fix extended-query isolation for pooled postgres.js clients in pglite-socket#977sorenbs wants to merge 1 commit intoelectric-sql:mainfrom
sorenbs wants to merge 1 commit intoelectric-sql:mainfrom
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This fixes a protocol-isolation bug in
packages/pglite-socketthat shows up when apostgres.jspool uses multiple concurrent connections againstPGLiteSocketServer.The core problem was that the socket server queued and scheduled individual frontend protocol messages globally across handlers, but only pinned handler ownership while
db.isInTransaction()was true. That is not enough for the PostgreSQL extended query protocol, because unnamed prepared-statement state lives until the backend reachesReadyForQuery, not only while SQL transaction state is open.In practice, message sequences like
Parse/Bind/Execute/Syncfrom different logical clients could interleave against the same backend session state. That produced errors like:PostgresError: unnamed prepared statement does not existcode: 26000routine: exec_bind_messageHow we hit this
I ran into this while working on a Prisma Dev / Durable Streams demo that used a PGlite-backed Postgres runtime behind
pglite-socket.When opening Prisma Studio against that runtime, the first load would sometimes show a red
introspecterror and then succeed on refresh. Prisma Studio usespostgres.js, and its initial page load issues concurrent metadata/introspection-style queries. One of the concurrent queries is a timezone read:Against
pglite-socket, that concurrent startup pattern could fail even though the same queries worked:max: 1That suggested the bug was below Prisma Dev and below the WAL/Streams layer.
Reproduction
I isolated this down to plain
PGLiteSocketServer, with no Prisma Dev and no WAL stream involved.Studio-shaped repro observed during investigation
The failure pattern that matched Prisma Studio was:
PGLiteSocketServer({ maxConnections: 10 })postgres(url, { max: 10 })select current_setting('timezone') as timezone26000 / exec_bind_messageRegression test added in this PR
For a stable package test, this PR adds a smaller repro that isolates the same protocol bug without depending on Prisma internals:
sql.unsafe('select $1::int as value', [i])sql.unsafe("select current_setting('timezone') as timezone", [])run concurrently through a
postgres.jspool withmax: 10.Before this fix, that failed reliably in local verification. After this fix, it passes consistently.
Root cause
QueryQueueManagerwas effectively serializing individual frontend messages, not whole extended-query exchanges.That meant handler A could send
Parseand then handler B could get scheduled before handler A reachedSync/ReadyForQuery. Because the backend session is shared, the unnamed statement/portal state could be overwritten or cleared before handler A's laterBind/Execute, which explains theunnamed prepared statement does not existfailure.Fix
This change introduces handler ownership at the protocol level:
activeHandlerIdReadyForQueryReadyForQueryreturns to idle (I)Terminatepackets so shutdown/connection close does not strand other queued workThis keeps logical client state isolated even though the underlying backend session is shared.
Verification
Ran:
Result locally:
pglite-socketsuite passes (60tests)Files
packages/pglite-socket/src/index.tspackages/pglite-socket/tests/query-with-postgres-js-concurrency.test.tsIf helpful, I can also add a second regression that uses a more Studio-shaped catalog query pair, but I kept the committed test minimal and protocol-focused.