Skip to content

Commit

Permalink
In long-running processes, allows checking the database connection by…
Browse files Browse the repository at this point in the history
… pinging it, thus avoiding exceptions. Additionally, it provides the flexibility to configure the frequency of the check to be performed in the case of long-running processes.
  • Loading branch information
alli83 committed Apr 11, 2024
1 parent 140ecec commit 3a7e486
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 5 deletions.
12 changes: 12 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class Configuration

private ?SchemaManagerFactory $schemaManagerFactory = null;

private ?int $checkConnectionTiming = null;

public function __construct()
{
$this->schemaAssetsFilter = static function (): bool {
Expand Down Expand Up @@ -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;
}
}
38 changes: 37 additions & 1 deletion src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*
Expand All @@ -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();
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Check warning on line 1407 in src/Connection.php

View check run for this annotation

Codecov / codecov/patch

src/Connection.php#L1406-L1407

Added lines #L1406 - L1407 were not covered by tests
}
}
}
41 changes: 41 additions & 0 deletions tests/Functional/Connection/ConnectionReactivatedTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Connection;

use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Tests\FunctionalTestCase;

use function sleep;

class ConnectionReactivatedTest extends FunctionalTestCase
{
public static function setUpBeforeClass(): void
{
self::markConnectionWithHeartBeat();
}

protected function setUp(): void
{
if ($this->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));
}
}
9 changes: 8 additions & 1 deletion tests/FunctionalTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
Expand Down
10 changes: 7 additions & 3 deletions tests/TestUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -75,7 +75,7 @@ public static function getConnection(): Connection

return DriverManager::getConnection(
$params,
self::createConfiguration($params['driver']),
self::createConfiguration($params['driver'], $hasHeartBeat),
);
}

Expand Down Expand Up @@ -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();

Expand All @@ -170,6 +170,10 @@ private static function createConfiguration(string $driver): Configuration

$configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory());

if ($hasHearBeat) {
$configuration->setCheckConnectionTiming(1);
}

return $configuration;
}

Expand Down

0 comments on commit 3a7e486

Please sign in to comment.