From 1a8b607f544e08965567a95135052e3ef4f78d3a Mon Sep 17 00:00:00 2001 From: "benoit.condaminet" Date: Wed, 17 Jun 2026 15:34:21 +0200 Subject: [PATCH] Forward libpq TCP keepalive and timeout parameters in the PostgreSQL drivers Add support for the libpq connection keywords keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout and connect_timeout in both the pdo_pgsql and pgsql drivers, in the same style as the existing ssl* keywords. These bound how long a connection can block when a TCP session is silently dropped, which otherwise hangs until the kernel exhausts net.ipv4.tcp_retries2 (~15 min by default). Refs #7408 --- docs/en/reference/configuration.rst | 18 ++++++++++++++++++ src/Driver/PDO/PgSQL/Driver.php | 24 ++++++++++++++++++++++++ src/Driver/PgSQL/Driver.php | 6 ++++++ tests/Driver/PDO/PgSQL/DriverTest.php | 17 +++++++++++++++++ tests/Driver/PgSQL/DriverTest.php | 18 ++++++++++++++++++ 5 files changed, 83 insertions(+) diff --git a/docs/en/reference/configuration.rst b/docs/en/reference/configuration.rst index 7001c5e9abe..93d43661915 100644 --- a/docs/en/reference/configuration.rst +++ b/docs/en/reference/configuration.rst @@ -287,6 +287,24 @@ pdo_pgsql / pgsql - ``persistent`` (boolean): Whether to establish a persistent connection (currently supported only by ``pdo_pgsql``). - ``application_name`` (string): Name of the application that is connecting to database. Optional. It will be displayed at ``pg_stat_activity``. +- ``connect_timeout`` (integer): Maximum time to wait while connecting, in seconds. + See `www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CONNECT-TIMEOUT `_ +- ``keepalives`` (integer): Controls whether client-side TCP keepalives are used (``1`` to + enable, ``0`` to disable). + See `www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-KEEPALIVES `_ +- ``keepalives_idle`` (integer): Number of seconds of inactivity after which a TCP keepalive + message is sent to the server. +- ``keepalives_interval`` (integer): Number of seconds after which an unacknowledged TCP + keepalive message is retransmitted. +- ``keepalives_count`` (integer): Number of TCP keepalives that can be lost before the + connection is considered dead. +- ``tcp_user_timeout`` (integer): Number of milliseconds that transmitted data may remain + unacknowledged before the connection is forcibly closed. Useful to bound how long a + query blocks when a connection is silently dropped. + See `www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TCP-USER-TIMEOUT `_ + +These keepalive and timeout parameters are forwarded to libpq and apply to both the +``pdo_pgsql`` and ``pgsql`` drivers. PostgreSQL behaves differently with regard to booleans when you use ``PDO::ATTR_EMULATE_PREPARES`` or not. To switch from using ``'true'`` diff --git a/src/Driver/PDO/PgSQL/Driver.php b/src/Driver/PDO/PgSQL/Driver.php index f7016b7ad50..834d396e35b 100644 --- a/src/Driver/PDO/PgSQL/Driver.php +++ b/src/Driver/PDO/PgSQL/Driver.php @@ -126,6 +126,30 @@ private function constructPdoDsn(array $params): string $dsn .= 'gssencmode=' . $params['gssencmode'] . ';'; } + if (isset($params['connect_timeout'])) { + $dsn .= 'connect_timeout=' . $params['connect_timeout'] . ';'; + } + + if (isset($params['keepalives'])) { + $dsn .= 'keepalives=' . $params['keepalives'] . ';'; + } + + if (isset($params['keepalives_idle'])) { + $dsn .= 'keepalives_idle=' . $params['keepalives_idle'] . ';'; + } + + if (isset($params['keepalives_interval'])) { + $dsn .= 'keepalives_interval=' . $params['keepalives_interval'] . ';'; + } + + if (isset($params['keepalives_count'])) { + $dsn .= 'keepalives_count=' . $params['keepalives_count'] . ';'; + } + + if (isset($params['tcp_user_timeout'])) { + $dsn .= 'tcp_user_timeout=' . $params['tcp_user_timeout'] . ';'; + } + return $dsn; } } diff --git a/src/Driver/PgSQL/Driver.php b/src/Driver/PgSQL/Driver.php index 6bdcb609904..9481e8ed333 100644 --- a/src/Driver/PgSQL/Driver.php +++ b/src/Driver/PgSQL/Driver.php @@ -85,6 +85,12 @@ private function constructConnectionString( 'password' => $params['password'] ?? null, 'sslmode' => $params['sslmode'] ?? null, 'gssencmode' => $params['gssencmode'] ?? null, + 'connect_timeout' => $params['connect_timeout'] ?? null, + 'keepalives' => $params['keepalives'] ?? null, + 'keepalives_idle' => $params['keepalives_idle'] ?? null, + 'keepalives_interval' => $params['keepalives_interval'] ?? null, + 'keepalives_count' => $params['keepalives_count'] ?? null, + 'tcp_user_timeout' => $params['tcp_user_timeout'] ?? null, ], static fn (int|string|null $value) => $value !== '' && $value !== null, ); diff --git a/tests/Driver/PDO/PgSQL/DriverTest.php b/tests/Driver/PDO/PgSQL/DriverTest.php index 3e898c112e9..8e3edb5ebc7 100644 --- a/tests/Driver/PDO/PgSQL/DriverTest.php +++ b/tests/Driver/PDO/PgSQL/DriverTest.php @@ -60,6 +60,23 @@ public function testConnectionDisablePreparesWhenDisablePreparesIsExplicitlyDefi ); } + public function testConnectionWithTcpKeepaliveParameters(): void + { + self::expectNotToPerformAssertions(); + + $this->createDriver()->connect(array_merge( + TestUtil::getConnectionParams(), + [ + 'connect_timeout' => 5, + 'keepalives' => 1, + 'keepalives_idle' => 10, + 'keepalives_interval' => 5, + 'keepalives_count' => 3, + 'tcp_user_timeout' => 15000, + ], + )); + } + public function testUserIsFalse(): void { $this->expectException(InvalidConfiguration::class); diff --git a/tests/Driver/PgSQL/DriverTest.php b/tests/Driver/PgSQL/DriverTest.php index 47ea35c17e0..8cf218fd61f 100644 --- a/tests/Driver/PgSQL/DriverTest.php +++ b/tests/Driver/PgSQL/DriverTest.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Tests\Driver\AbstractDriverTestCase; use Doctrine\DBAL\Tests\TestUtil; +use function array_merge; use function in_array; class DriverTest extends AbstractDriverTestCase @@ -42,6 +43,23 @@ public function testConnectionIPv6(): void $this->driver->connect($params); } + public function testConnectionWithTcpKeepaliveParameters(): void + { + self::expectNotToPerformAssertions(); + + $this->driver->connect(array_merge( + TestUtil::getConnectionParams(), + [ + 'connect_timeout' => 5, + 'keepalives' => 1, + 'keepalives_idle' => 10, + 'keepalives_interval' => 5, + 'keepalives_count' => 3, + 'tcp_user_timeout' => 15000, + ], + )); + } + protected function createDriver(): DriverInterface { return new Driver();