From fcc550b33052fbe2d926031573774a02857821cd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 17 Sep 2025 00:19:19 +1200 Subject: [PATCH 1/4] Enable reconnection + retry --- src/Database/PDO.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Database/PDO.php b/src/Database/PDO.php index 069ef88f8..ebcc8bbfa 100644 --- a/src/Database/PDO.php +++ b/src/Database/PDO.php @@ -3,6 +3,7 @@ namespace Utopia\Database; use InvalidArgumentException; +use Utopia\CLI\Console; /** * A PDO wrapper that forwards method calls to the internal PDO instance. @@ -41,7 +42,24 @@ public function __construct( */ public function __call(string $method, array $args): mixed { - return $this->pdo->{$method}(...$args); + try { + return $this->pdo->{$method}(...$args); + } catch (\Throwable $e) { + if (Connection::hasError($e)) { + Console::warning('[Database] Lost connection detected. Reconnecting...'); + + // Attempt to reconnect + $this->reconnect(); + + // If we're not in a transaction, also retry the query + // In a transaction we can't retry as it would lead to data integrity issues + if (!$this->pdo->inTransaction()) { + return $this->pdo->{$method}(...$args); + } + } + + throw $e; + } } /** From 6496e63aa2bdf30ce9b6f91f84f347b43fabc2ee Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 17 Sep 2025 00:21:09 +1200 Subject: [PATCH 2/4] Enable reconnect test --- tests/unit/PDOTest.php | 76 +++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/unit/PDOTest.php b/tests/unit/PDOTest.php index a17de1eb3..45e9a12a2 100644 --- a/tests/unit/PDOTest.php +++ b/tests/unit/PDOTest.php @@ -41,44 +41,44 @@ public function testMethodCallIsForwardedToPDO(): void $this->assertSame($pdoStatementMock, $result); } - // public function testLostConnectionRetriesCall(): void - // { - // $dsn = 'sqlite::memory:'; - // $pdoWrapper = $this->getMockBuilder(PDO::class) - // ->setConstructorArgs([$dsn, null, null, []]) - // ->onlyMethods(['reconnect']) - // ->getMock(); - // - // $pdoMock = $this->getMockBuilder(\PDO::class) - // ->disableOriginalConstructor() - // ->getMock(); - // $pdoStatementMock = $this->getMockBuilder(\PDOStatement::class) - // ->disableOriginalConstructor() - // ->getMock(); - // - // $pdoMock->expects($this->exactly(2)) - // ->method('query') - // ->with('SELECT 1') - // ->will($this->onConsecutiveCalls( - // $this->throwException(new \Exception("Lost connection")), - // $pdoStatementMock - // )); - // - // $reflection = new ReflectionClass($pdoWrapper); - // $pdoProperty = $reflection->getProperty('pdo'); - // $pdoProperty->setAccessible(true); - // $pdoProperty->setValue($pdoWrapper, $pdoMock); - // - // $pdoWrapper->expects($this->once()) - // ->method('reconnect') - // ->willReturnCallback(function () use ($pdoWrapper, $pdoMock, $pdoProperty) { - // $pdoProperty->setValue($pdoWrapper, $pdoMock); - // }); - // - // $result = $pdoWrapper->query('SELECT 1'); - // - // $this->assertSame($pdoStatementMock, $result); - // } + public function testLostConnectionRetriesCall(): void + { + $dsn = 'sqlite::memory:'; + $pdoWrapper = $this->getMockBuilder(PDO::class) + ->setConstructorArgs([$dsn, null, null, []]) + ->onlyMethods(['reconnect']) + ->getMock(); + + $pdoMock = $this->getMockBuilder(\PDO::class) + ->disableOriginalConstructor() + ->getMock(); + $pdoStatementMock = $this->getMockBuilder(\PDOStatement::class) + ->disableOriginalConstructor() + ->getMock(); + + $pdoMock->expects($this->exactly(2)) + ->method('query') + ->with('SELECT 1') + ->will($this->onConsecutiveCalls( + $this->throwException(new \Exception("Lost connection")), + $pdoStatementMock + )); + + $reflection = new ReflectionClass($pdoWrapper); + $pdoProperty = $reflection->getProperty('pdo'); + $pdoProperty->setAccessible(true); + $pdoProperty->setValue($pdoWrapper, $pdoMock); + + $pdoWrapper->expects($this->once()) + ->method('reconnect') + ->willReturnCallback(function () use ($pdoWrapper, $pdoMock, $pdoProperty) { + $pdoProperty->setValue($pdoWrapper, $pdoMock); + }); + + $result = $pdoWrapper->query('SELECT 1'); + + $this->assertSame($pdoStatementMock, $result); + } public function testNonLostConnectionExceptionIsRethrown(): void { From 8f2840d55dcf71bdbb7ddd1ea1cfad64008bf0a5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 17 Sep 2025 00:56:31 +1200 Subject: [PATCH 3/4] Check in transaction before reconnect --- src/Database/PDO.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Database/PDO.php b/src/Database/PDO.php index ebcc8bbfa..c00f7b073 100644 --- a/src/Database/PDO.php +++ b/src/Database/PDO.php @@ -48,12 +48,14 @@ public function __call(string $method, array $args): mixed if (Connection::hasError($e)) { Console::warning('[Database] Lost connection detected. Reconnecting...'); + $inTransaction = $this->pdo->inTransaction(); + // Attempt to reconnect $this->reconnect(); - // If we're not in a transaction, also retry the query - // In a transaction we can't retry as it would lead to data integrity issues - if (!$this->pdo->inTransaction()) { + // If we weren't in a transaction, also retry the query + // In a transaction we can't retry as the state is attached to the previous connection + if (!$inTransaction) { return $this->pdo->{$method}(...$args); } } From 9a046f232524582dd84b0e3be9d6bac34444366c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 17 Sep 2025 01:25:47 +1200 Subject: [PATCH 4/4] Log connection error message --- src/Database/PDO.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/PDO.php b/src/Database/PDO.php index c00f7b073..245b0dfad 100644 --- a/src/Database/PDO.php +++ b/src/Database/PDO.php @@ -46,6 +46,7 @@ public function __call(string $method, array $args): mixed return $this->pdo->{$method}(...$args); } catch (\Throwable $e) { if (Connection::hasError($e)) { + Console::warning('[Database] ' . $e->getMessage()); Console::warning('[Database] Lost connection detected. Reconnecting...'); $inTransaction = $this->pdo->inTransaction();