From f6b116abdcc92986d0a51b4a7e9f362fa95c5213 Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Thu, 27 Aug 2026 13:07:24 +0200 Subject: [PATCH] Tolerate deallocation of statements the server has already dropped --- src/Internal/PgSqlHandle.php | 12 +++++++- src/Internal/PqHandle.php | 9 +++++- test/StatementDeallocateTest.php | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 test/StatementDeallocateTest.php diff --git a/src/Internal/PgSqlHandle.php b/src/Internal/PgSqlHandle.php index 022a09a..797be27 100644 --- a/src/Internal/PgSqlHandle.php +++ b/src/Internal/PgSqlHandle.php @@ -413,7 +413,17 @@ public function statementDeallocate(string $name): void return; // Statement already deallocated. } - $this->query(\sprintf("DEALLOCATE %s", $name)); + try { + // Send the DEALLOCATE without building a result object: an error result + // must not be processed through createResult(), whose fatal-error drain + // would consume results belonging to an operation dispatched after this + // one and leave that operation awaiting a response forever. + $this->send(\pg_send_query(...), \sprintf("DEALLOCATE %s", $name)); + } catch (\Throwable) { + // Ignore failures: the server may have already dropped the statement, + // e.g. via the DISCARD ALL issued when the pool resets connections. + } + unset($this->statements[$name]); }); $storage->future->ignore(); diff --git a/src/Internal/PqHandle.php b/src/Internal/PqHandle.php index a71f5bb..f46512d 100644 --- a/src/Internal/PqHandle.php +++ b/src/Internal/PqHandle.php @@ -338,9 +338,16 @@ public function statementDeallocate(string $name): void return; // Statement already deallocated. } - $this->send(null, $statement->deallocateAsync(...)); + try { + $this->send(null, $statement->deallocateAsync(...)); + } catch (\Throwable) { + // Ignore failures: the server may have already dropped the statement, + // e.g. via the DISCARD ALL issued when the pool resets connections. + } + unset($this->statements[$name]); }); + $storage->future->ignore(); } #[\Override] diff --git a/test/StatementDeallocateTest.php b/test/StatementDeallocateTest.php new file mode 100644 index 0000000..5c186cf --- /dev/null +++ b/test/StatementDeallocateTest.php @@ -0,0 +1,51 @@ +getHandle() instanceof \EvLoop) { + $this->markTestSkipped("ext-pgsql is not compatible with pecl-ev"); + } + + $this->setTimeout(5); + + $pool = new PostgresConnectionPool( + PostgresConfig::fromString('host=localhost user=postgres password=postgres'), + 1, + ); + + try { + $statement = $pool->prepare('SELECT 1 AS value'); + $result = $statement->execute(); + \iterator_to_array($result); + + // Dropping the statement and result queues a DEALLOCATE for the prepared + // statement, while the pool's DISCARD ALL reset on the next checkout drops + // it server-side. The failing DEALLOCATE must not consume responses that + // belong to operations dispatched after it. + unset($result, $statement); + + for ($i = 1; $i <= 3; ++$i) { + $rows = \iterator_to_array($pool->query(\sprintf('SELECT %d AS value', $i))); + self::assertSame($i, $rows[0]['value']); + } + + $statement = $pool->prepare('SELECT 2 AS value'); + $rows = \iterator_to_array($statement->execute()); + self::assertSame(2, $rows[0]['value']); + } finally { + $pool->close(); + } + } +}