diff --git a/src/boost/docs/database.md b/src/boost/docs/database.md index 14e73a03b..0c691246c 100644 --- a/src/boost/docs/database.md +++ b/src/boost/docs/database.md @@ -112,9 +112,10 @@ To see how read / write connections should be configured, let's look at this exa 'max_connections' => (int) env('DB_MAX_CONNECTIONS', 10), 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'heartbeat_timeout' => 1.0, + 'heartbeat' => (float) env('DB_HEARTBEAT', -1), + 'heartbeat_timeout' => (float) env('DB_HEARTBEAT_TIMEOUT', 1.0), 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), + 'max_lifetime' => (float) env('DB_MAX_LIFETIME', -1), ], ], ``` @@ -144,14 +145,17 @@ Each connection may define its own `pool` configuration: 'max_connections' => (int) env('DB_MAX_CONNECTIONS', 10), 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'heartbeat_timeout' => 1.0, + 'heartbeat' => (float) env('DB_HEARTBEAT', -1), + 'heartbeat_timeout' => (float) env('DB_HEARTBEAT_TIMEOUT', 1.0), 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), + 'max_lifetime' => (float) env('DB_MAX_LIFETIME', -1), ], ], ``` -The `min_connections` option determines the minimum number of connections kept warm in the pool, while the `max_connections` option determines the maximum number of connections that may be opened for the worker. The `connect_timeout` option controls how long Hypervel will wait while opening a new database connection. The `wait_timeout` option controls how long a coroutine may wait for an available connection when the pool is exhausted. The `heartbeat` option controls how often Hypervel validates idle pooled connections; set this value to `-1` to disable heartbeats. When heartbeats are enabled, Hypervel keeps the minimum idle connection set alive with a raw `SELECT 1` ping that does not fire query events, query logs, or query duration handlers. The `heartbeat_timeout` option controls how long a heartbeat ping may run before the connection is discarded. The `max_idle_time` option controls how long idle connections above the minimum pool size may remain in the pool before they are closed. +The `min_connections` option determines the minimum number of connections kept warm in the pool, while the `max_connections` option determines the maximum number of connections that may be opened for the worker. The `connect_timeout` option controls how long Hypervel will wait while opening a new database connection. The `wait_timeout` option controls how long a coroutine may wait for an available connection when the pool is exhausted. The `heartbeat` option controls how often Hypervel validates idle connections in the worker pool; set this value to `-1` to disable heartbeats. When heartbeats are enabled, Hypervel keeps the minimum idle connection set alive with a raw `SELECT 1` ping that does not fire query events, query logs, or query duration handlers. The `heartbeat_timeout` option controls how long a heartbeat ping may run before the connection is discarded. The `max_idle_time` option controls how long idle connections above the minimum pool size may remain in the pool before they are closed. The `max_lifetime` option controls how long a pooled connection generation may live before it is recycled while idle or before it is reused; set this value to `-1` to disable lifetime recycling. + +Heartbeat and max lifetime recycling apply to Hypervel's worker pool whether the connection points directly at the database or through a proxy / pooler. They help long-running workers avoid stale sockets and rotate old idle connection generations before those connections are used by a request. Hypervel's default database configuration also includes a `pgsql-pooled` connection. This connection is intended for PostgreSQL transaction poolers such as PgBouncer and uses separate `DB_POOLED_*` environment variables. It also sets `migrations_connection` to `pgsql`, allowing your application to use the pooled connection at runtime while migration commands use the direct PostgreSQL connection. diff --git a/src/boost/todo.md b/src/boost/todo.md index 1f3230bcf..4a6bf0247 100644 --- a/src/boost/todo.md +++ b/src/boost/todo.md @@ -25,7 +25,7 @@ ## Database -- Add max-lifetime recycling support for pooled database connections. Heartbeat keeps minimum idle connections validated for the worker lifetime, but rotating database poolers and managed proxies may still benefit from periodically replacing otherwise healthy long-lived sockets. Correct fix: add an opt-in pool option that recycles idle pooled DB connections older than the configured lifetime without affecting borrowed connections or normal query execution. +- Consider built-in early jitter for DB pool `max_lifetime`. Exact max-lifetime recycling can make a burst-created cohort of idle connections expire in the same heartbeat tick, causing synchronized reconnects. A clean design would avoid an extra config knob and assign each connection an effective lifetime between 90-100% of `max_lifetime`, so the configured value remains the upper bound while reconnects are spread out. ## Foundation @@ -52,6 +52,7 @@ ## Pool - Make `Hypervel\Pool\KeepaliveConnection` honor disabled heartbeat configuration. `PoolOption` documents `heartbeat => -1` as disabled, but `KeepaliveConnection::getHeartbeatSeconds()` currently turns any non-positive heartbeat into a 10-second interval and `addHeartbeat()` always creates a timer. Correct fix: only create the heartbeat timer when `PoolOption::getHeartbeat() > 0`; when heartbeat is `<= 0`, do not start a timer or run heartbeat work. Keep `max_idle_time` behavior separate from heartbeat. +- Add Redis pool heartbeat and max-lifetime support. Redis pools expose a `heartbeat` config key today, but `Hypervel\Redis\RedisPool` extends the base pool and no Redis code consumes `PoolOption::getHeartbeat()` or starts a heartbeat timer. Correct fix: add opt-in Redis heartbeat and max-lifetime recycling for idle pooled Redis connections, keep both disabled by default, and test that borrowed connections are never recycled. ## Routing diff --git a/src/contracts/src/Pool/PoolOptionInterface.php b/src/contracts/src/Pool/PoolOptionInterface.php index 62a0c4c8e..1cc1bd5ec 100644 --- a/src/contracts/src/Pool/PoolOptionInterface.php +++ b/src/contracts/src/Pool/PoolOptionInterface.php @@ -41,6 +41,11 @@ public function getHeartbeatTimeout(): float; */ public function getMaxIdleTime(): float; + /** + * Get the maximum lifetime in seconds before a connection is recycled. + */ + public function getMaxLifetime(): float; + /** * Get the events to trigger on connection lifecycle. */ diff --git a/src/database/src/Pool/DbPool.php b/src/database/src/Pool/DbPool.php index d1e583ed6..e06afc54c 100644 --- a/src/database/src/Pool/DbPool.php +++ b/src/database/src/Pool/DbPool.php @@ -201,7 +201,15 @@ protected function heartbeat(): void protected function heartbeatConnection(PooledConnection $connection): void { try { - if ($connection->isIdleExpired() && $this->currentConnections > $this->option->getMinConnections()) { + $now = microtime(true); + + if ($connection->isLifetimeExpired($now)) { + $this->discardHeartbeatConnection($connection); + + return; + } + + if ($connection->isIdleExpired($now) && $this->currentConnections > $this->option->getMinConnections()) { $this->discardHeartbeatConnection($connection); return; diff --git a/src/database/src/Pool/PooledConnection.php b/src/database/src/Pool/PooledConnection.php index 384d6f79b..3df5ffb5b 100644 --- a/src/database/src/Pool/PooledConnection.php +++ b/src/database/src/Pool/PooledConnection.php @@ -45,6 +45,10 @@ class PooledConnection implements PoolConnectionInterface protected float $lastReleaseTime = 0.0; + protected float $createdAt = 0.0; + + protected bool $availableForReuse = false; + protected bool $invalid = false; protected ?Dispatcher $dispatcher = null; @@ -78,6 +82,8 @@ public function getConnection(): Connection public function getActiveConnection(): Connection { if ($this->check()) { + $this->availableForReuse = false; + return $this->connection; } @@ -133,7 +139,10 @@ public function reconnect(): bool ); } - $this->lastUseTime = microtime(true); + $now = microtime(true); + $this->lastUseTime = $now; + $this->createdAt = $now; + $this->availableForReuse = false; $this->markValid(); return true; @@ -152,14 +161,23 @@ public function check(): bool return false; } - $maxIdleTime = $this->pool->getOption()->getMaxIdleTime(); $now = microtime(true); - if ($now > $maxIdleTime + max($this->lastReleaseTime, $this->lastUseTime)) { - return false; - } + if ($this->availableForReuse) { + // Time-based recycling is a reuse rule; it must not replace a connection + // while the borrowed wrapper may still hold transaction state. + if ($this->isLifetimeExpired($now)) { + return false; + } - $this->lastUseTime = $now; + $maxIdleTime = $this->pool->getOption()->getMaxIdleTime(); + + if ($now > $maxIdleTime + max($this->lastReleaseTime, $this->lastUseTime)) { + return false; + } + + $this->lastUseTime = $now; + } return true; } @@ -270,6 +288,7 @@ public function release(): void // Mark as stale so it will be recreated $this->markInvalid(); } finally { + $this->availableForReuse = true; $this->pool->release($this); } } @@ -290,6 +309,28 @@ public function getLastReleaseTime(): float return $this->lastReleaseTime; } + /** + * Get the connection generation creation time. + */ + public function getCreatedAt(): float + { + return $this->createdAt; + } + + /** + * Determine if this connection generation has reached its maximum lifetime. + */ + public function isLifetimeExpired(?float $now = null): bool + { + $maxLifetime = $this->pool->getOption()->getMaxLifetime(); + + if ($maxLifetime <= 0) { + return false; + } + + return ($now ?? microtime(true)) >= $this->createdAt + $maxLifetime; + } + /** * Determine if the underlying connection has an open transaction. */ @@ -399,5 +440,7 @@ protected function refresh(Connection $connection): void new ConnectionEstablished($connection) ); } + + $this->createdAt = microtime(true); } } diff --git a/src/foundation/config/database.php b/src/foundation/config/database.php index 60a304456..c6d573e7c 100644 --- a/src/foundation/config/database.php +++ b/src/foundation/config/database.php @@ -25,8 +25,8 @@ |-------------------------------------------------------------------------- | | Database connections may define a "pool" array for long-lived workers. - | Heartbeats are disabled with -1; positive values keep the minimum - | idle connections validated without firing query events or logs. + | Heartbeats validate idle connections, while max lifetime recycling + | rotates old idle connection generations before they are reused. | */ @@ -79,9 +79,10 @@ 'max_connections' => (int) env('DB_MAX_CONNECTIONS', 10), 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'heartbeat_timeout' => 1.0, + 'heartbeat' => (float) env('DB_HEARTBEAT', -1), + 'heartbeat_timeout' => (float) env('DB_HEARTBEAT_TIMEOUT', 1.0), 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), + 'max_lifetime' => (float) env('DB_MAX_LIFETIME', -1), ], ], @@ -108,9 +109,10 @@ 'max_connections' => (int) env('DB_MAX_CONNECTIONS', 10), 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'heartbeat_timeout' => 1.0, + 'heartbeat' => (float) env('DB_HEARTBEAT', -1), + 'heartbeat_timeout' => (float) env('DB_HEARTBEAT_TIMEOUT', 1.0), 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), + 'max_lifetime' => (float) env('DB_MAX_LIFETIME', -1), ], ], @@ -135,9 +137,10 @@ 'max_connections' => (int) env('DB_MAX_CONNECTIONS', 10), 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'heartbeat_timeout' => 1.0, + 'heartbeat' => (float) env('DB_HEARTBEAT', -1), + 'heartbeat_timeout' => (float) env('DB_HEARTBEAT_TIMEOUT', 1.0), 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), + 'max_lifetime' => (float) env('DB_MAX_LIFETIME', -1), ], ], @@ -163,9 +166,10 @@ 'max_connections' => (int) env('DB_POOLED_MAX_CONNECTIONS', 20), 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'heartbeat_timeout' => 1.0, + 'heartbeat' => (float) env('DB_POOLED_HEARTBEAT', -1), + 'heartbeat_timeout' => (float) env('DB_POOLED_HEARTBEAT_TIMEOUT', 1.0), 'max_idle_time' => (float) env('DB_POOLED_MAX_IDLE_TIME', 60), + 'max_lifetime' => (float) env('DB_POOLED_MAX_LIFETIME', -1), ], ], ], diff --git a/src/pool/src/Pool.php b/src/pool/src/Pool.php index 65c622dfc..7f762b302 100644 --- a/src/pool/src/Pool.php +++ b/src/pool/src/Pool.php @@ -173,6 +173,7 @@ protected function initOption(array $options = []): void heartbeat: $options['heartbeat'] ?? -1, heartbeatTimeout: $options['heartbeat_timeout'] ?? 1.0, maxIdleTime: $options['max_idle_time'] ?? 60.0, + maxLifetime: $options['max_lifetime'] ?? -1.0, events: $options['events'] ?? [], ); } diff --git a/src/pool/src/PoolOption.php b/src/pool/src/PoolOption.php index 7606271b3..2965b9ffc 100644 --- a/src/pool/src/PoolOption.php +++ b/src/pool/src/PoolOption.php @@ -19,6 +19,7 @@ class PoolOption implements PoolOptionInterface * @param float $heartbeat Heartbeat interval in seconds (-1 to disable) * @param float $heartbeatTimeout Heartbeat timeout in seconds * @param float $maxIdleTime Maximum idle time in seconds before connection is closed + * @param float $maxLifetime Maximum lifetime in seconds before connection is recycled (-1 to disable) * @param array $events Events to trigger on connection lifecycle */ public function __construct( @@ -29,6 +30,7 @@ public function __construct( private float $heartbeat = -1, private float $heartbeatTimeout = 1.0, private float $maxIdleTime = 60.0, + private float $maxLifetime = -1.0, private array $events = [], ) { } @@ -166,6 +168,28 @@ public function setMaxIdleTime(float $maxIdleTime): static return $this; } + /** + * Get the maximum lifetime in seconds before a connection is recycled. + */ + public function getMaxLifetime(): float + { + return $this->maxLifetime; + } + + /** + * Set the maximum lifetime in seconds before a connection is recycled. + * + * Boot-only. The value persists on the worker-lifetime pool option and is + * read by every subsequent pool operation. Per-request use races across + * coroutines. + */ + public function setMaxLifetime(float $maxLifetime): static + { + $this->maxLifetime = $maxLifetime; + + return $this; + } + public function getEvents(): array { return $this->events; diff --git a/tests/Integration/Database/PooledConnectionTest.php b/tests/Integration/Database/PooledConnectionTest.php index 5c25dbb33..441355193 100644 --- a/tests/Integration/Database/PooledConnectionTest.php +++ b/tests/Integration/Database/PooledConnectionTest.php @@ -41,7 +41,9 @@ protected function defineEnvironment(ApplicationContract $app): void 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, + 'heartbeat_timeout' => 1.0, 'max_idle_time' => 60.0, + 'max_lifetime' => -1.0, ], ]); } @@ -262,17 +264,28 @@ function () use (&$fired) { $this->assertTrue($fired, 'ReleaseConnection event should be dispatched when configured'); } - public function testLastUseTimeUpdatedOnCheck(): void + public function testLastUseTimeUpdatedOnReuseCheck(): void { $pool = new DbPool($this->app, 'pool_test'); - $pooledConnection = $this->createPooledConnection($pool); + + /** @var PooledConnection $pooledConnection */ + $pooledConnection = $pool->get(); + $pooledConnection->getConnection(); $initialTime = $pooledConnection->getLastUseTime(); + $pooledConnection->release(); + usleep(10000); // 10ms - $pooledConnection->check(); - $this->assertGreaterThan($initialTime, $pooledConnection->getLastUseTime()); + /** @var PooledConnection $nextPooledConnection */ + $nextPooledConnection = $pool->get(); + $nextPooledConnection->getConnection(); + + $this->assertSame($pooledConnection, $nextPooledConnection); + $this->assertGreaterThan($initialTime, $nextPooledConnection->getLastUseTime()); + + $nextPooledConnection->release(); } public function testInvalidConnectionReconnectsEvenWithFreshReleaseTime(): void @@ -287,6 +300,103 @@ public function testInvalidConnectionReconnectsEvenWithFreshReleaseTime(): void $this->assertNotSame($originalConnection, $pooledConnection->getActiveConnection()); } + public function testExpiredLifetimeDoesNotReconnectDuringActiveBorrow(): void + { + $this->app->make('config')->set('database.connections.pool_test.pool.max_lifetime', 1.0); + + $pool = new DbPool($this->app, 'pool_test'); + $pooledConnection = $this->createPooledConnection($pool); + $originalConnection = $pooledConnection->getConnection(); + + $this->assertSame(1.0, $pool->getOption()->getMaxLifetime()); + + $originalConnection->beginTransaction(); + $this->ageConnectionGeneration($pooledConnection); + + $this->assertTrue($pooledConnection->check()); + $this->assertSame($originalConnection, $pooledConnection->getActiveConnection()); + $this->assertSame(1, $originalConnection->transactionLevel()); + + $originalConnection->rollBack(); + } + + public function testExpiredIdleTimeDoesNotReconnectDuringActiveBorrow(): void + { + $this->app->make('config')->set('database.connections.pool_test.pool.max_idle_time', 1.0); + + $pool = new DbPool($this->app, 'pool_test'); + $pooledConnection = $this->createPooledConnection($pool); + $originalConnection = $pooledConnection->getConnection(); + + $this->ageActiveConnectionUse($pooledConnection); + + $this->assertTrue($pooledConnection->check()); + $this->assertSame($originalConnection, $pooledConnection->getActiveConnection()); + } + + public function testExpiredLifetimeReconnectsWhenBorrowedFromPoolAgainWithoutHeartbeat(): void + { + $this->app->make('config')->set('database.connections.pool_test.pool.max_lifetime', 1.0); + + $pool = new DbPool($this->app, 'pool_test'); + + /** @var PooledConnection $pooledConnection */ + $pooledConnection = $pool->get(); + $originalConnection = $pooledConnection->getConnection(); + $pooledConnection->release(); + + $this->ageConnectionGeneration($pooledConnection); + + /** @var PooledConnection $nextPooledConnection */ + $nextPooledConnection = $pool->get(); + + $this->assertSame($pooledConnection, $nextPooledConnection); + $this->assertNotSame($originalConnection, $nextPooledConnection->getConnection()); + + $nextPooledConnection->release(); + } + + public function testDisabledMaxLifetimeDoesNotRecycleAgedConnectionGeneration(): void + { + $pool = new DbPool($this->app, 'pool_test'); + $pooledConnection = $this->createPooledConnection($pool); + $originalConnection = $pooledConnection->getConnection(); + + $this->assertSame(-1.0, $pool->getOption()->getMaxLifetime()); + + $this->ageConnectionGeneration($pooledConnection); + + $this->assertFalse($pooledConnection->isLifetimeExpired()); + $this->assertTrue($pooledConnection->check()); + $this->assertSame($originalConnection, $pooledConnection->getActiveConnection()); + } + + public function testPingDoesNotExtendConnectionLifetime(): void + { + $pool = new DbPool($this->app, 'pool_test'); + $pooledConnection = $this->createPooledConnection($pool); + $pooledConnection->getConnection()->getPdo(); + + $createdAt = $pooledConnection->getCreatedAt(); + + $this->assertTrue($pooledConnection->ping(1.0)); + $this->assertSame($createdAt, $pooledConnection->getCreatedAt()); + } + + public function testConnectionRefreshResetsLifetime(): void + { + $pool = new DbPool($this->app, 'pool_test'); + $pooledConnection = $this->createPooledConnection($pool); + $connection = $pooledConnection->getConnection(); + + $this->ageConnectionGeneration($pooledConnection); + $expiredAt = $pooledConnection->getCreatedAt(); + + $connection->reconnect(); + + $this->assertGreaterThan($expiredAt, $pooledConnection->getCreatedAt()); + } + public function testReleaseSnapshotsErrorCountBeforeResettingConnection(): void { $pool = new DbPool($this->app, 'pool_test'); @@ -440,4 +550,14 @@ private function createPooledConnectionForName(DbPool $pool, string $name): Pool return new PooledConnection($this->app, $pool, $config); } + + private function ageConnectionGeneration(PooledConnection $connection): void + { + (new ReflectionProperty(PooledConnection::class, 'createdAt'))->setValue($connection, microtime(true) - 5.0); + } + + private function ageActiveConnectionUse(PooledConnection $connection): void + { + (new ReflectionProperty(PooledConnection::class, 'lastUseTime'))->setValue($connection, microtime(true) - 5.0); + } } diff --git a/tests/Integration/Database/Sqlite/DbPoolHeartbeatTest.php b/tests/Integration/Database/Sqlite/DbPoolHeartbeatTest.php index 321a8573e..ed62ca860 100644 --- a/tests/Integration/Database/Sqlite/DbPoolHeartbeatTest.php +++ b/tests/Integration/Database/Sqlite/DbPoolHeartbeatTest.php @@ -146,6 +146,62 @@ public function testHeartbeatValidationKeepsMinimumConnectionCheckoutValid(): vo }); } + public function testHeartbeatDiscardsLifetimeExpiredIdleConnectionBeforePinging(): void + { + run(function () { + $pool = $this->createPool([ + 'min_connections' => 1, + 'max_connections' => 1, + 'heartbeat' => -1, + 'max_lifetime' => 1.0, + ], LifetimeExpiredPingTrackingDbPool::class); + + $pooledConnection = $pool->get(); + $this->assertInstanceOf(LifetimeExpiredPingTrackingPooledConnection::class, $pooledConnection); + + $connection = $pooledConnection->getConnection(); + $pooledConnection->release(); + + $this->ageConnectionGeneration($pooledConnection); + + $pool->runHeartbeatForTest(); + + $this->assertFalse($pooledConnection->pingCalled); + $this->assertSame(0, $pool->getCurrentConnections()); + $this->assertSame(0, $pool->getConnectionsInChannel()); + + $nextPooledConnection = $pool->get(); + + $this->assertNotSame($connection, $nextPooledConnection->getConnection()); + + $nextPooledConnection->release(); + }); + } + + public function testHeartbeatDoesNotRecycleBorrowedLifetimeExpiredConnection(): void + { + run(function () { + $pool = $this->createPool([ + 'min_connections' => 1, + 'max_connections' => 1, + 'heartbeat' => -1, + 'max_lifetime' => 1.0, + ]); + + $borrowed = $pool->get(); + + $this->ageConnectionGeneration($borrowed); + + $pool->runHeartbeatForTest(); + + $this->assertSame(1, $borrowed->getConnection()->selectOne('SELECT 1 as result')->result); + $this->assertSame(1, $pool->getCurrentConnections()); + $this->assertSame(0, $pool->getConnectionsInChannel()); + + $borrowed->release(); + }); + } + public function testHeartbeatDoesNotRealizeLazyPdoClosures(): void { run(function () { @@ -353,6 +409,7 @@ protected function createPool(array $poolOptions = [], string $poolClass = Inspe 'heartbeat' => -1, 'heartbeat_timeout' => 1.0, 'max_idle_time' => 60.0, + 'max_lifetime' => -1.0, ...$poolOptions, ], ]); @@ -371,6 +428,11 @@ protected function ageReleasedConnection(PooledConnection $connection): void $lastReleaseTime->setValue($connection, microtime(true) - 5.0); $lastUseTime->setValue($connection, microtime(true) - 5.0); } + + protected function ageConnectionGeneration(PooledConnection $connection): void + { + (new ReflectionProperty(PooledConnection::class, 'createdAt'))->setValue($connection, microtime(true) - 5.0); + } } class InspectableHeartbeatDbPool extends DbPool @@ -404,6 +466,26 @@ public function ping(float $timeout): bool } } +class LifetimeExpiredPingTrackingDbPool extends InspectableHeartbeatDbPool +{ + protected function createConnection(): ConnectionInterface + { + return new LifetimeExpiredPingTrackingPooledConnection($this->container, $this, $this->config); + } +} + +class LifetimeExpiredPingTrackingPooledConnection extends PooledConnection +{ + public bool $pingCalled = false; + + public function ping(float $timeout): bool + { + $this->pingCalled = true; + + return true; + } +} + class FlushingHeartbeatDbPool extends InspectableHeartbeatDbPool { protected function createConnection(): ConnectionInterface diff --git a/tests/Pool/PoolOptionTest.php b/tests/Pool/PoolOptionTest.php new file mode 100644 index 000000000..980b98571 --- /dev/null +++ b/tests/Pool/PoolOptionTest.php @@ -0,0 +1,33 @@ +assertSame(-1.0, $option->getMaxLifetime()); + } + + public function testMaxLifetimeCanBeConfigured(): void + { + $option = new PoolOption(maxLifetime: 120.0); + + $this->assertSame(120.0, $option->getMaxLifetime()); + } + + public function testMaxLifetimeCanBeChanged(): void + { + $option = new PoolOption; + + $this->assertSame($option, $option->setMaxLifetime(30.0)); + $this->assertSame(30.0, $option->getMaxLifetime()); + } +}