diff --git a/src/Configuration.php b/src/Configuration.php index 9aa001db483..3267a96e570 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -36,6 +36,8 @@ class Configuration private ?SchemaManagerFactory $schemaManagerFactory = null; + private ?int $checkConnectionTiming = null; + public function __construct() { $this->schemaAssetsFilter = static function (): bool { @@ -153,4 +155,14 @@ public function setDisableTypeComments(bool $disableTypeComments): self return $this; } + + public function setCheckConnectionTiming(int $timing): void + { + $this->checkConnectionTiming = $timing; + } + + public function getCheckConnectionTiming(): ?int + { + return $this->checkConnectionTiming; + } } diff --git a/src/Connection.php b/src/Connection.php index 3bb951445ff..4eccd7aa373 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -42,6 +42,7 @@ use function is_string; use function key; use function sprintf; +use function time; /** * A database abstraction-level connection that implements features like transaction isolation levels, @@ -99,6 +100,12 @@ class Connection implements ServerVersionProvider private SchemaManagerFactory $schemaManagerFactory; + private bool $isChecking = false; + + private int $lastCheckedAt = 0; + + private ?int $heartbeat; + /** * Initializes a new instance of the Connection class. * @@ -119,6 +126,8 @@ public function __construct( $this->params = $params; $this->autoCommit = $this->_config->getAutoCommit(); + $this->heartbeat = $this->_config->getCheckConnectionTiming(); + $this->schemaManagerFactory = $this->_config->getSchemaManagerFactory() ?? new DefaultSchemaManagerFactory(); } @@ -210,7 +219,23 @@ public function createExpressionBuilder(): ExpressionBuilder protected function connect(): DriverConnection { if ($this->_conn !== null) { - return $this->_conn; + $isTimeToCheck = time() - $this->lastCheckedAt >= $this->heartbeat; + $noCheckNeeded = $this->heartbeat === null || $this->isChecking; + + if ($noCheckNeeded || ! $isTimeToCheck) { + return $this->_conn; + } + + $this->isChecking = true; + + $isAvailable = $this->reconnectOnFailure(); + + $this->lastCheckedAt = time(); + $this->isChecking = false; + + if ($isAvailable) { + return $this->_conn; + } } try { @@ -1371,4 +1396,15 @@ private function handleDriverException( return $exception; } + + private function reconnectOnFailure(): bool + { + try { + $this->executeQuery($this->getDatabasePlatform()->getDummySelectSQL()); + + return true; + } catch (ConnectionLost) { + return false; + } + } } diff --git a/tests/Functional/Connection/ConnectionReactivatedTest.php b/tests/Functional/Connection/ConnectionReactivatedTest.php new file mode 100644 index 00000000000..88cdca94c46 --- /dev/null +++ b/tests/Functional/Connection/ConnectionReactivatedTest.php @@ -0,0 +1,41 @@ +connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + return; + } + + self::markTestSkipped('Currently only supported with MySQL'); + } + + public function testConnectionReactivated(): void + { + $this->connection->executeStatement('SET SESSION wait_timeout=1'); + + sleep(2); + + $query = $this->connection->getDatabasePlatform() + ->getDummySelectSQL(); + + $this->connection->executeQuery($query); + + self::assertEquals(1, $this->connection->fetchOne($query)); + } +} diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php index 29428454ccf..2d96a6712b1 100644 --- a/tests/FunctionalTestCase.php +++ b/tests/FunctionalTestCase.php @@ -26,6 +26,8 @@ abstract class FunctionalTestCase extends TestCase */ private bool $isConnectionReusable = true; + protected static bool $hasHeartBeat = false; + /** * Mark shared connection not reusable for subsequent tests. * @@ -37,11 +39,16 @@ protected function markConnectionNotReusable(): void $this->isConnectionReusable = false; } + protected static function markConnectionWithHeartBeat(): void + { + self::$hasHeartBeat = true; + } + #[Before] final protected function connect(): void { if (self::$sharedConnection === null) { - self::$sharedConnection = TestUtil::getConnection(); + self::$sharedConnection = TestUtil::getConnection(self::$hasHeartBeat); } $this->connection = self::$sharedConnection; diff --git a/tests/TestUtil.php b/tests/TestUtil.php index 354ce38ef87..4da2e66d8a2 100644 --- a/tests/TestUtil.php +++ b/tests/TestUtil.php @@ -62,7 +62,7 @@ class TestUtil * * @return Connection The database connection instance. */ - public static function getConnection(): Connection + public static function getConnection(bool $hasHeartBeat = false): Connection { $params = self::getConnectionParams(); @@ -75,7 +75,7 @@ public static function getConnection(): Connection return DriverManager::getConnection( $params, - self::createConfiguration($params['driver']), + self::createConfiguration($params['driver'], $hasHeartBeat), ); } @@ -153,7 +153,7 @@ private static function initializeDatabase(): void $privConn->close(); } - private static function createConfiguration(string $driver): Configuration + private static function createConfiguration(string $driver, bool $hasHearBeat): Configuration { $configuration = new Configuration(); @@ -170,6 +170,10 @@ private static function createConfiguration(string $driver): Configuration $configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + if ($hasHearBeat) { + $configuration->setCheckConnectionTiming(1); + } + return $configuration; }