Skip to content

Commit 0c54e19

Browse files
committed
Force-close previous (dis)connection when opening new database
1 parent 5d58189 commit 0c54e19

File tree

2 files changed

+125
-7
lines changed

2 files changed

+125
-7
lines changed

src/Io/LazyDatabase.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class LazyDatabase extends EventEmitter implements DatabaseInterface
1919
private $loop;
2020

2121
private $closed = false;
22+
/**@var ?DatabaseInterface */
23+
private $disconnecting;
2224
/** @var ?\React\Promise\PromiseInterface */
2325
private $promise;
2426
private $idlePeriod = 60.0;
@@ -51,6 +53,12 @@ private function db()
5153
return \React\Promise\reject(new \RuntimeException('Connection closed'));
5254
}
5355

56+
// force-close connection if still waiting for previous disconnection
57+
if ($this->disconnecting !== null) {
58+
$this->disconnecting->close();
59+
$this->disconnecting = null;
60+
}
61+
5462
$this->promise = $promise = $this->factory->open($this->filename, $this->flags);
5563
$promise->then(function (DatabaseInterface $db) {
5664
// connection completed => remember only until closed
@@ -127,6 +135,12 @@ public function close()
127135

128136
$this->closed = true;
129137

138+
// force-close connection if still waiting for previous disconnection
139+
if ($this->disconnecting !== null) {
140+
$this->disconnecting->close();
141+
$this->disconnecting = null;
142+
}
143+
130144
// either close active connection or cancel pending connection attempt
131145
if ($this->promise !== null) {
132146
$this->promise->then(function (DatabaseInterface $db) {
@@ -164,7 +178,18 @@ private function idle()
164178
if ($this->pending < 1 && $this->idlePeriod >= 0) {
165179
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
166180
$this->promise->then(function (DatabaseInterface $db) {
167-
$db->close();
181+
$this->disconnecting = $db;
182+
$db->quit()->then(
183+
function () {
184+
// successfully disconnected => remove reference
185+
$this->disconnecting = null;
186+
},
187+
function () use ($db) {
188+
// soft-close failed => force-close connection
189+
$db->close();
190+
$this->disconnecting = null;
191+
}
192+
);
168193
});
169194
$this->promise = null;
170195
$this->idleTimer = null;

tests/Io/LazyDatabaseTest.php

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,36 @@ public function testExecAfterExecWillStartAndCancelIdleTimerWhenSecondExecStarts
234234
$this->db->exec('CREATE');
235235
}
236236

237-
public function testExecFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent()
237+
public function testExecFollowedByIdleTimerWillQuitUnderlyingConnectionWithoutCloseEvent()
238238
{
239-
$client = $this->getMockBuilder('Clue\React\SQLite\Io\ProcessIoDatabase')->disableOriginalConstructor()->setMethods(array('exec', 'close'))->getMock();
239+
$client = $this->getMockBuilder('Clue\React\SQLite\Io\ProcessIoDatabase')->disableOriginalConstructor()->setMethods(array('exec', 'quit', 'close'))->getMock();
240+
$client->expects($this->once())->method('exec')->willReturn(\React\Promise\resolve());
241+
$client->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve());
242+
$client->expects($this->never())->method('close');
243+
244+
$this->factory->expects($this->once())->method('open')->willReturn(\React\Promise\resolve($client));
245+
246+
$timeout = null;
247+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
248+
$this->loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
249+
$timeout = $cb;
250+
return true;
251+
}))->willReturn($timer);
252+
253+
$this->db->on('close', $this->expectCallableNever());
254+
255+
$this->db->exec('CREATE');
256+
257+
$this->assertNotNull($timeout);
258+
$timeout();
259+
}
260+
261+
public function testExecFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuitFails()
262+
{
263+
$client = $this->getMockBuilder('Clue\React\SQLite\Io\ProcessIoDatabase')->setMethods(array('exec', 'quit', 'close'))->disableOriginalConstructor()->getMock();
240264
$client->expects($this->once())->method('exec')->willReturn(\React\Promise\resolve());
241-
$client->expects($this->once())->method('close')->willReturn(\React\Promise\resolve());
265+
$client->expects($this->once())->method('quit')->willReturn(\React\Promise\reject());
266+
$client->expects($this->once())->method('close');
242267

243268
$this->factory->expects($this->once())->method('open')->willReturn(\React\Promise\resolve($client));
244269

@@ -257,6 +282,35 @@ public function testExecFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutC
257282
$timeout();
258283
}
259284

285+
public function testExecAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection()
286+
{
287+
$client = $this->getMockBuilder('Clue\React\SQLite\Io\ProcessIoDatabase')->setMethods(array('exec', 'quit', 'close'))->disableOriginalConstructor()->getMock();
288+
$client->expects($this->once())->method('exec')->willReturn(\React\Promise\resolve());
289+
$client->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
290+
$client->expects($this->once())->method('close');
291+
292+
$this->factory->expects($this->exactly(2))->method('open')->willReturnOnConsecutiveCalls(
293+
\React\Promise\resolve($client),
294+
new Promise(function () { })
295+
);
296+
297+
$timeout = null;
298+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
299+
$this->loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
300+
$timeout = $cb;
301+
return true;
302+
}))->willReturn($timer);
303+
304+
$this->db->on('close', $this->expectCallableNever());
305+
306+
$this->db->exec('CREATE');
307+
308+
$this->assertNotNull($timeout);
309+
$timeout();
310+
311+
$this->db->exec('CREATE');
312+
}
313+
260314
public function testQueryWillCreateUnderlyingDatabaseAndReturnPendingPromise()
261315
{
262316
$promise = new Promise(function () { });
@@ -407,11 +461,12 @@ public function testQueryAfterQueryWillStartAndCancelIdleTimerWhenSecondQuerySta
407461
$this->db->query('CREATE');
408462
}
409463

410-
public function testQueryFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent()
464+
public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionWithoutCloseEvent()
411465
{
412-
$client = $this->getMockBuilder('Clue\React\SQLite\Io\ProcessIoDatabase')->disableOriginalConstructor()->setMethods(array('query', 'close'))->getMock();
466+
$client = $this->getMockBuilder('Clue\React\SQLite\Io\ProcessIoDatabase')->disableOriginalConstructor()->setMethods(array('query', 'quit', 'close'))->getMock();
413467
$client->expects($this->once())->method('query')->willReturn(\React\Promise\resolve());
414-
$client->expects($this->once())->method('close')->willReturn(\React\Promise\resolve());
468+
$client->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve());
469+
$client->expects($this->never())->method('close');
415470

416471
$this->factory->expects($this->once())->method('open')->willReturn(\React\Promise\resolve($client));
417472

@@ -525,6 +580,44 @@ public function testCloseAfterExecRejectsWillEmitClose()
525580
$deferred->reject(new \RuntimeException());
526581
}
527582

583+
public function testCloseAfterQuitAfterExecWillCloseUnderlyingConnectionWhenQuitIsStillPending()
584+
{
585+
$client = $this->getMockBuilder('Clue\React\SQLite\DatabaseInterface')->getMock();
586+
$client->expects($this->once())->method('exec')->willReturn(\React\Promise\resolve());
587+
$client->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
588+
$client->expects($this->once())->method('close');
589+
590+
$this->factory->expects($this->once())->method('open')->willReturn(\React\Promise\resolve($client));
591+
592+
$this->db->exec('CREATE');
593+
$this->db->quit();
594+
$this->db->close();
595+
}
596+
597+
public function testCloseAfterExecAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending()
598+
{
599+
$client = $this->getMockBuilder('Clue\React\SQLite\DatabaseInterface')->getMock();
600+
$client->expects($this->once())->method('exec')->willReturn(\React\Promise\resolve());
601+
$client->expects($this->once())->method('quit')->willReturn(new Promise(function () { }));
602+
$client->expects($this->once())->method('close');
603+
604+
$this->factory->expects($this->once())->method('open')->willReturn(\React\Promise\resolve($client));
605+
606+
$timeout = null;
607+
$timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
608+
$this->loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) {
609+
$timeout = $cb;
610+
return true;
611+
}))->willReturn($timer);
612+
613+
$this->db->exec('CREATE');
614+
615+
$this->assertNotNull($timeout);
616+
$timeout();
617+
618+
$this->db->close();
619+
}
620+
528621
public function testQuitWillCloseDatabaseIfUnderlyingConnectionIsNotPendingAndResolveImmediately()
529622
{
530623
$this->db->on('close', $this->expectCallableOnce());

0 commit comments

Comments
 (0)