Skip to content

fix(state): don't wait for a ReadyForQuery the server will never send - #1356

Merged
jkaczman merged 2 commits into
mainfrom
jk-savepoint-rfq-fix
Aug 13, 2026
Merged

fix(state): don't wait for a ReadyForQuery the server will never send#1356
jkaczman merged 2 commits into
mainfrom
jk-savepoint-rfq-fix

Conversation

@jkaczman

@jkaczman jkaczman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

A client using Postgrex's mode: :savepoint hangs on any query that errors. The error is returned to the client, but the client never gets the ReadyForQuery that lets it show the error to the caller. It hangs until reaching timeout.

Postgrex sends Bind (extended), Execute (extended), and a simple Query("RELEASE SAVEPOINT ...") as one batch. It doesn't include a Sync in that batch, it does afterwards though, as part of error recovery.

When the Execute errors, the server gets rid of all messages until a Sync comes. This includes the RELEASE query previously mentioned. However, due to the mixed nature of the batch (extended, extended, simple), PgDog expects a ReadyForQuery back...

PgDog queued the same expectation (Z) for both Sync-owed and Query-owed RFQ, so our error branch in state.rs's action method couldn't tell them apart. Thus, it re-queued the trailing RFQ unconditionally, and that being the only entry that survived kept has_more_messages() true in server.rs, so PgDog stayed infinitely stuck on the server socket, and never read the Sync the client had already sent.

Essentially a Deadlock: client waits on PgDog, PgDog waits on server, server waits on a Sync (which is sitting unread in PgDog's client socket).

PgDog 1314

What I changed:

  • Differentiated a Query-owed vs Sync-owed RFQ into two variants: ReadyForQuery and ReadyForQuerySync. After an extended-protocol error, the server only ever answers the Sync, so we need a way to distinguish them.
  • Modified the error branch to classify by the front queue entry, which belongs to the request the server was processing. This is the same test the server itself uses to decide whether or not to skip to a Sync.
  • On an extended error, expectations are dropped up to the first Sync-owed RFQ. If no Sync is queued yet (this bug!), the queue drains and PgDog goes back to the client, where it can now receive the Sync.
  • Removed the sticky extended flag that represented the connection seeing extended traffic, not that the error occurred in an extended message.
  • Split old 'C' | 's' | 'I' lump into CommandComplete and ExecutionCompleted. CommandComplete can come from a simple Query or from an extended Execute. An Execute one means error skips to a sync, so we need to differentiate there too.

Tests: added two state.rs regression tests, three new Elixir integration tests, did some small updates to some state.rs tests to update to the new variants.

Fixes #1314

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.57143% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pgdog/src/backend/protocol/state.rs 98.46% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@jkaczman
jkaczman marked this pull request as ready for review August 13, 2026 17:38

@levkk levkk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Very cool!

@jkaczman
jkaczman merged commit 7d055cd into main Aug 13, 2026
28 checks passed
@jkaczman
jkaczman deleted the jk-savepoint-rfq-fix branch August 13, 2026 19:16
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.

Savepoint mixed with opened portal breaks state on portal execution error

2 participants