Repository navigation
Fix cancelling a query that has not started - #1256
Open
GrahamCampbell wants to merge 1 commit into
Open
GrahamCampbell wants to merge 1 commit into
GrahamCampbell wants to merge 1 commit into
Conversation
GrahamCampbell
force-pushed
the
cancel-pipelined-query
branch
from
October 14, 2026 10:21
dbb16a3 to
8086136
Compare
This was referenced Oct 14, 2026
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.
A query cancelled while it waits piped behind another is skipped in
ReadyForQuery, but the server still answers it, so the query behind it takes that answer, every later answer on the connection lands one query late, and the cancelled query never settles (#1252). A query cancelled while its connection is still opening leaves that connection inconnectingwith nothing in flight, so the pool hands it nothing untilmax_lifetimeends it.This changes two lines in
ReadyForQuery. Thewhilebecomes anif, so a cancelled query stays current and settles with its own answer. Thereturnending theinitialbranch is removed, so a connection whose first query did not start falls through toonopen; the same fall-through lets areserve()that opens a connection withfetch_types: falseresolve (#1219).Fixes #1252
Mechanism, reproduction and output
A query that has been piped to a busy connection but has not started yet can be cancelled, and
cancel()then only records the request (query.cancelled = { resolve, reject }). When the query ahead of it finishes, the loop inReadyForQueryshifts the cancelled query offsent, sends its CancelRequest and, because the query was cancelled, goes round again and makes the next query current. The server still answers the cancelled query, since it already has its messages, so the next query receives that answer: the cancelled query's57014, or its rows if it finished before the CancelRequest arrived. Every later answer on the connection then lands one query late, and the cancelled query is in neitherquerynorsent, so nothing settles it.A query cancelled while its connection is still opening has a related problem in the same function. It is the connection's initial query and has no backend state yet, so
cancel()rejects it at once and marks it cancelled. When the connection is ready, theinitialbranch callsexecute(initial), which returns at once for a cancelled query, and then returns without reachingonopen, so the connection stays inconnectingwith nothing in flight. The pool does not hand it anything again untilmax_lifetimeends it, and withmax: 1every query waits that long.This changes two lines in
ReadyForQuery. Thewhilebecomes anif, so a cancelled query stays current with its CancelRequest sent and settles with its own answer at its own ReadyForQuery, where the next query is shifted the same way; two cancelled queries in a row are each handled in turn. Thereturnat the end of theinitialbranch is removed, so a connection whose initial query did not start falls through to the common tail and does what any ready connection does,onopen, orterminate()when the pool is ending. Whenexecute(initial)did start the query,if (query) returnreturns as before, andsentis empty on a connection that has just opened. The same fall-through lets areserve()that opens a connection withfetch_types: falseresolve (#1219, which #1220 addresses in the same lines). The CancelRequest still goes out when the cancelled query becomes current, so the race the README describes, where a CancelRequest that arrives after its query has finished can cancel a later query on that backend, is unchanged.Run against a plain PostgreSQL 17 server, with the connection taken from the
PG*environment variables:On master (411429e):
The cancelled query in the first case never settles, and the next query in the second is only rejected when
sql.end({ timeout: 0 })destroys the pool three seconds later. With this change:The two new tests, "Cancel piped query with a query piped behind it" and "Cancel query while its connection is opening", fail on master and pass with the change.