Repository navigation
Conversation
The poll callback completed an operation after its first result, and the
caller then fetched the remaining results with the blocking pg_get_result.
In between, another fiber could see no pending operation and send its own
query on the same handle; pg_get_result then blocked on that query and
returned its result as the previous query's next result, so the other
query's result was lost and its caller waited forever.
This happens with statement deallocation, which runs a DEALLOCATE query in
its own fiber while the pool considers the connection idle: a query sent
from {main} (whose suspension resumes before queued fiber resumptions)
while the DEALLOCATE is in flight hangs.
The poll callback now collects all results of an operation, until
pg_get_result returns false, before completing it, and the results are
built from that list, without blocking calls on the connection.
This was referenced Oct 19, 2026
Hjwad
pushed a commit
to Hjwad/MadelineProto
that referenced
this pull request
Oct 20, 2026
1.1.6 uses unnamed statements for Postgres queries: deallocating the named statement prepared for every query raced with the next queries on the connection in amphp/postgres (amphp/postgres#75), losing their results, so a cache flush issued from a destructor was lost or a query hung forever.
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.
The ext-pgsql handle can lose a query's result and hang its caller forever.
Mechanism. The poll callback completes an operation as soon as its first result is available (
$deferred->complete(\pg_get_result($connection)); $deferred = null;), and the caller then fetches the remaining results with the blockingpg_get_result()(createResult()→fetchNextResult(), and the drain loop in the error path). Between the two, the handle has no pending operation, so another fiber waiting insend()can send its own query. When the first caller then callspg_get_result(), libpq blocks on the new query, reads its response off the socket and returns it as the first query's next result. The second query's pending operation never completes: its response was consumed, so the socket never becomes readable again.This is easy to hit through statement deallocation, which runs
DEALLOCATEin its own fiber (statementDeallocate()) while the pool considers the connection idle: if a query is sent from{main}while theDEALLOCATEis in flight,{main}'s suspension is resumed before the queued resumption of the deallocating fiber, so{main}sends its query first, and the deallocating fiber'sfetchNextResult()steals its result. In a traced reproduction (aPostgresConnectionPoolwhosepop()sendsDISCARD ALL):after which the connection is not busy, its socket is not readable, and the
DISCARD ALLcaller waits forever. We found it through intermittent failures in danog/AsyncOrm's CI, where a write issued from a destructor was lost.Fix. The poll callback collects all results of the operation, until
pg_get_result()returnsfalse(the connection is idle again), before completing it, andcreateResult()builds the result chain from that list, without blocking calls on the connection (which also removes the blockingpg_get_result()calls from the event loop). COPY results, which libpq would return repeatedly, end the collection as before (they're unsupported and close the connection).Test.
PgSqlStatementDeallocateTestis a plainTestCaseso that it runs on{main}: it fails on the first iteration without the fix (the query's result is lost, a timer fails the test instead of hanging), and passes with it. The rest of the suite passes (ext-pq tests skipped locally); psalm reports the same issues as before.PqHandle::fetchNextResult()checksbusybeforegetResult(), so it doesn't block, but it may have a milder form of the same race (returning another operation's already-buffered result); I couldn't test ext-pq here.