Skip to content

Commit 8450eac

Browse files
committed
In long-running processes, allows checking the database connection by 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.
1 parent 140ecec commit 8450eac

File tree

4 files changed

+91
-3
lines changed

4 files changed

+91
-3
lines changed

src/Connection.php

+37-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use function is_string;
4343
use function key;
4444
use function sprintf;
45+
use function time;
4546

4647
/**
4748
* A database abstraction-level connection that implements features like transaction isolation levels,
@@ -99,6 +100,12 @@ class Connection implements ServerVersionProvider
99100

100101
private SchemaManagerFactory $schemaManagerFactory;
101102

103+
private bool $isChecking = false;
104+
105+
private ?int $lastCheckedAt = null;
106+
107+
private ?int $heartbeat;
108+
102109
/**
103110
* Initializes a new instance of the Connection class.
104111
*
@@ -115,6 +122,8 @@ public function __construct(
115122
protected Driver $driver,
116123
?Configuration $config = null,
117124
) {
125+
$this->heartbeat = $params['check_connection_frequency'] ?? null;
126+
118127
$this->_config = $config ?? new Configuration();
119128
$this->params = $params;
120129
$this->autoCommit = $this->_config->getAutoCommit();
@@ -210,7 +219,23 @@ public function createExpressionBuilder(): ExpressionBuilder
210219
protected function connect(): DriverConnection
211220
{
212221
if ($this->_conn !== null) {
213-
return $this->_conn;
222+
$isTimeToCheck = $this->lastCheckedAt === null || time() - $this->lastCheckedAt >= $this->heartbeat;
223+
$noCheckNeeded = $this->heartbeat === null || $this->isChecking;
224+
225+
if ($noCheckNeeded || ! $isTimeToCheck) {
226+
return $this->_conn;
227+
}
228+
229+
$this->isChecking = true;
230+
231+
$isAvailable = $this->reconnectOnFailure();
232+
233+
$this->lastCheckedAt = time();
234+
$this->isChecking = false;
235+
236+
if ($isAvailable) {
237+
return $this->_conn;
238+
}
214239
}
215240

216241
try {
@@ -1371,4 +1396,15 @@ private function handleDriverException(
13711396

13721397
return $exception;
13731398
}
1399+
1400+
private function reconnectOnFailure(): bool
1401+
{
1402+
try {
1403+
$this->executeQuery($this->getDatabasePlatform($this)->getDummySelectSQL());
1404+
1405+
return true;
1406+
} catch (ConnectionLost) {
1407+
return false;
1408+
}
1409+
}
13741410
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\Connection;
6+
7+
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
8+
use Doctrine\DBAL\Tests\FunctionalTestCase;
9+
10+
use function sleep;
11+
12+
class ConnectionReactivatedTest extends FunctionalTestCase
13+
{
14+
public static function setUpBeforeClass(): void
15+
{
16+
self::markConnectionWithHeartBeat();
17+
}
18+
19+
protected function setUp(): void
20+
{
21+
if ($this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
22+
return;
23+
}
24+
25+
self::markTestSkipped('Currently only supported with MySQL');
26+
}
27+
28+
public function testConnectionReactivated(): void
29+
{
30+
$this->connection->executeStatement('SET SESSION wait_timeout=1');
31+
32+
sleep(2);
33+
34+
$query = $this->connection->getDatabasePlatform()
35+
->getDummySelectSQL();
36+
37+
$this->connection->executeQuery($query);
38+
39+
self::assertEquals(1, $this->connection->fetchOne($query));
40+
}
41+
}

tests/FunctionalTestCase.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ abstract class FunctionalTestCase extends TestCase
2626
*/
2727
private bool $isConnectionReusable = true;
2828

29+
protected static bool $hasHeartBeat = false;
30+
2931
/**
3032
* Mark shared connection not reusable for subsequent tests.
3133
*
@@ -37,11 +39,16 @@ protected function markConnectionNotReusable(): void
3739
$this->isConnectionReusable = false;
3840
}
3941

42+
protected static function markConnectionWithHeartBeat(): void
43+
{
44+
self::$hasHeartBeat = true;
45+
}
46+
4047
#[Before]
4148
final protected function connect(): void
4249
{
4350
if (self::$sharedConnection === null) {
44-
self::$sharedConnection = TestUtil::getConnection();
51+
self::$sharedConnection = TestUtil::getConnection(self::$hasHeartBeat);
4552
}
4653

4754
$this->connection = self::$sharedConnection;

tests/TestUtil.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ class TestUtil
6262
*
6363
* @return Connection The database connection instance.
6464
*/
65-
public static function getConnection(): Connection
65+
public static function getConnection($hasHeartBeat = false): Connection
6666
{
6767
$params = self::getConnectionParams();
6868

69+
if ($hasHeartBeat) {
70+
$params['check_connection_frequency'] = 1;
71+
}
72+
6973
if (empty($params['memory']) && ! self::$initialized) {
7074
self::initializeDatabase();
7175
self::$initialized = true;

0 commit comments

Comments
 (0)