Skip to content

Commit fe0e975

Browse files
authored
Merge pull request #532 from utopia-php/feat-auto-reconnect
Feat auto reconnect
2 parents 3a034f8 + ce0005d commit fe0e975

File tree

3 files changed

+205
-53
lines changed

3 files changed

+205
-53
lines changed

composer.lock

Lines changed: 39 additions & 52 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Database/PDO.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Utopia\Database;
44

5+
use Swoole\Database\DetectsLostConnections;
6+
57
/**
68
* A PDO wrapper that forwards method calls to the internal PDO instance.
79
*
@@ -35,10 +37,21 @@ public function __construct(
3537
* @param string $method
3638
* @param array<mixed> $args
3739
* @return mixed
40+
* @throws \Throwable
3841
*/
3942
public function __call(string $method, array $args): mixed
4043
{
41-
return $this->pdo->{$method}(...$args);
44+
try {
45+
return $this->pdo->{$method}(...$args);
46+
} catch (\Throwable $e) {
47+
/** @phpstan-ignore-next-line can't find static method */
48+
if (DetectsLostConnections::causedByLostConnection($e)) {
49+
$this->reconnect();
50+
return $this->pdo->{$method}(...$args);
51+
}
52+
53+
throw $e;
54+
}
4255
}
4356

4457
/**

tests/unit/PDOTest.php

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
3+
namespace Tests\Unit;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use ReflectionClass;
7+
use Utopia\Database\PDO;
8+
9+
class PDOTest extends TestCase
10+
{
11+
public function testMethodCallIsForwardedToPDO(): void
12+
{
13+
$dsn = 'sqlite::memory:';
14+
$pdoWrapper = new PDO($dsn, null, null);
15+
16+
// Use Reflection to replace the internal PDO instance with a mock
17+
$reflection = new ReflectionClass($pdoWrapper);
18+
$pdoProperty = $reflection->getProperty('pdo');
19+
$pdoProperty->setAccessible(true);
20+
21+
// Create a mock for the internal \PDO object.
22+
$pdoMock = $this->getMockBuilder(\PDO::class)
23+
->disableOriginalConstructor()
24+
->getMock();
25+
26+
// Create a PDOStatement mock since query returns a PDOStatement
27+
$pdoStatementMock = $this->getMockBuilder(\PDOStatement::class)
28+
->disableOriginalConstructor()
29+
->getMock();
30+
31+
// Expect that when we call 'query', the mock returns our PDOStatement mock.
32+
$pdoMock->expects($this->once())
33+
->method('query')
34+
->with('SELECT 1')
35+
->willReturn($pdoStatementMock);
36+
37+
$pdoProperty->setValue($pdoWrapper, $pdoMock);
38+
39+
$result = $pdoWrapper->query('SELECT 1');
40+
41+
$this->assertSame($pdoStatementMock, $result);
42+
}
43+
44+
public function testLostConnectionRetriesCall(): void
45+
{
46+
$dsn = 'sqlite::memory:';
47+
$pdoWrapper = $this->getMockBuilder(PDO::class)
48+
->setConstructorArgs([$dsn, null, null, []])
49+
->onlyMethods(['reconnect'])
50+
->getMock();
51+
52+
$pdoMock = $this->getMockBuilder(\PDO::class)
53+
->disableOriginalConstructor()
54+
->getMock();
55+
$pdoStatementMock = $this->getMockBuilder(\PDOStatement::class)
56+
->disableOriginalConstructor()
57+
->getMock();
58+
59+
$pdoMock->expects($this->exactly(2))
60+
->method('query')
61+
->with('SELECT 1')
62+
->will($this->onConsecutiveCalls(
63+
$this->throwException(new \Exception("Lost connection")),
64+
$pdoStatementMock
65+
));
66+
67+
$reflection = new ReflectionClass($pdoWrapper);
68+
$pdoProperty = $reflection->getProperty('pdo');
69+
$pdoProperty->setAccessible(true);
70+
$pdoProperty->setValue($pdoWrapper, $pdoMock);
71+
72+
$pdoWrapper->expects($this->once())
73+
->method('reconnect')
74+
->willReturnCallback(function () use ($pdoWrapper, $pdoMock, $pdoProperty) {
75+
$pdoProperty->setValue($pdoWrapper, $pdoMock);
76+
});
77+
78+
$result = $pdoWrapper->query('SELECT 1');
79+
80+
$this->assertSame($pdoStatementMock, $result);
81+
}
82+
83+
public function testNonLostConnectionExceptionIsRethrown(): void
84+
{
85+
$dsn = 'sqlite::memory:';
86+
$pdoWrapper = new PDO($dsn, null, null);
87+
88+
$reflection = new ReflectionClass($pdoWrapper);
89+
$pdoProperty = $reflection->getProperty('pdo');
90+
$pdoProperty->setAccessible(true);
91+
92+
$pdoMock = $this->getMockBuilder(\PDO::class)
93+
->disableOriginalConstructor()
94+
->getMock();
95+
96+
$pdoMock->expects($this->once())
97+
->method('query')
98+
->with('SELECT 1')
99+
->will($this->throwException(new \Exception("Other error")));
100+
101+
$pdoProperty->setValue($pdoWrapper, $pdoMock);
102+
103+
$this->expectException(\Exception::class);
104+
$this->expectExceptionMessage("Other error");
105+
106+
$pdoWrapper->query('SELECT 1');
107+
}
108+
109+
public function testReconnectCreatesNewPDOInstance(): void
110+
{
111+
$dsn = 'sqlite::memory:';
112+
$pdoWrapper = new PDO($dsn, null, null);
113+
114+
$reflection = new ReflectionClass($pdoWrapper);
115+
$pdoProperty = $reflection->getProperty('pdo');
116+
$pdoProperty->setAccessible(true);
117+
118+
$oldPDO = $pdoProperty->getValue($pdoWrapper);
119+
$pdoWrapper->reconnect();
120+
$newPDO = $pdoProperty->getValue($pdoWrapper);
121+
122+
$this->assertNotSame($oldPDO, $newPDO, "Reconnect should create a new PDO instance");
123+
}
124+
125+
public function testMethodCallForPrepare(): void
126+
{
127+
$dsn = 'sqlite::memory:';
128+
$pdoWrapper = new PDO($dsn, null, null);
129+
130+
$reflection = new ReflectionClass($pdoWrapper);
131+
$pdoProperty = $reflection->getProperty('pdo');
132+
$pdoProperty->setAccessible(true);
133+
134+
$pdoMock = $this->getMockBuilder(\PDO::class)
135+
->disableOriginalConstructor()
136+
->getMock();
137+
$pdoStatementMock = $this->getMockBuilder(\PDOStatement::class)
138+
->disableOriginalConstructor()
139+
->getMock();
140+
141+
$pdoMock->expects($this->once())
142+
->method('prepare')
143+
->with('SELECT * FROM table', [\PDO::ATTR_CURSOR => \PDO::CURSOR_FWDONLY])
144+
->willReturn($pdoStatementMock);
145+
146+
$pdoProperty->setValue($pdoWrapper, $pdoMock);
147+
148+
$result = $pdoWrapper->prepare('SELECT * FROM table', [\PDO::ATTR_CURSOR => \PDO::CURSOR_FWDONLY]);
149+
150+
$this->assertSame($pdoStatementMock, $result);
151+
}
152+
}

0 commit comments

Comments
 (0)