Skip to content

Commit 740e97a

Browse files
committed
Keep track of underlying connection and create new when connection lost
1 parent d0b0c0b commit 740e97a

File tree

5 files changed

+147
-66
lines changed

5 files changed

+147
-66
lines changed

README.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -171,24 +171,33 @@ $connection->query(…);
171171
This method immediately returns a "virtual" connection implementing the
172172
[`ConnectionInterface`](#connectioninterface) that can be used to
173173
interface with your MySQL database. Internally, it lazily creates the
174-
underlying database connection (which may take some time) only once the
175-
first request is invoked on this instance and will queue all outstanding
176-
requests until the underlying connection is ready.
174+
underlying database connection only on demand once the first request is
175+
invoked on this instance and will queue all outstanding requests until
176+
the underlying connection is ready. Additionally, it will keep track of
177+
this underlying connection and will create a new underlying connection
178+
on demand when the current connection is lost.
177179

178180
From a consumer side this means that you can start sending queries to the
179-
database right away while the actual connection may still be outstanding.
180-
It will ensure that all commands will be executed in the order they are
181-
enqueued once the connection is ready. If the database connection fails,
182-
it will emit an `error` event, reject all outstanding commands and `close`
183-
the connection as described in the `ConnectionInterface`. In other words,
184-
it behaves just like a real connection and frees you from having to deal
185-
with its async resolution.
181+
database right away while the underlying connection may still be
182+
outstanding. Because creating this underlying connection may take some
183+
time, it will enqueue all oustanding commands and will ensure that all
184+
commands will be executed in correct order once the connection is ready.
185+
In other words, this "virtual" connection behaves just like a "real"
186+
connection as described in the `ConnectionInterface` and frees you from
187+
having to deal with its async resolution.
188+
189+
If the underlying database connection fails, it will reject all
190+
outstanding commands and will return to the initial "idle" state. This
191+
means that you can keep sending additional commands at a later time which
192+
will again try to open the underlying connection.
186193

187194
Note that creating the underlying connection will be deferred until the
188195
first request is invoked. Accordingly, any eventual connection issues
189-
will be detected once this instance is first used. Similarly, calling
190-
`quit()` on this instance before invoking any requests will succeed
191-
immediately and will not wait for an actual underlying connection.
196+
will be detected once this instance is first used. You can use the
197+
`quit()` method to ensure that the "virtual" connection will be soft-closed
198+
and no further commands can be enqueued. Similarly, calling `quit()` on
199+
this instance before invoking any requests will succeed immediately and
200+
will not wait for an actual underlying connection.
192201

193202
Depending on your particular use case, you may prefer this method or the
194203
underlying `createConnection()` which resolves with a promise. For many

src/Factory.php

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -211,24 +211,33 @@ public function createConnection($uri)
211211
* This method immediately returns a "virtual" connection implementing the
212212
* [`ConnectionInterface`](#connectioninterface) that can be used to
213213
* interface with your MySQL database. Internally, it lazily creates the
214-
* underlying database connection (which may take some time) only once the
215-
* first request is invoked on this instance and will queue all outstanding
216-
* requests until the underlying connection is ready.
214+
* underlying database connection only on demand once the first request is
215+
* invoked on this instance and will queue all outstanding requests until
216+
* the underlying connection is ready. Additionally, it will keep track of
217+
* this underlying connection and will create a new underlying connection
218+
* on demand when the current connection is lost.
217219
*
218220
* From a consumer side this means that you can start sending queries to the
219-
* database right away while the actual connection may still be outstanding.
220-
* It will ensure that all commands will be executed in the order they are
221-
* enqueued once the connection is ready. If the database connection fails,
222-
* it will emit an `error` event, reject all outstanding commands and `close`
223-
* the connection as described in the `ConnectionInterface`. In other words,
224-
* it behaves just like a real connection and frees you from having to deal
225-
* with its async resolution.
221+
* database right away while the underlying connection may still be
222+
* outstanding. Because creating this underlying connection may take some
223+
* time, it will enqueue all oustanding commands and will ensure that all
224+
* commands will be executed in correct order once the connection is ready.
225+
* In other words, this "virtual" connection behaves just like a "real"
226+
* connection as described in the `ConnectionInterface` and frees you from
227+
* having to deal with its async resolution.
228+
*
229+
* If the underlying database connection fails, it will reject all
230+
* outstanding commands and will return to the initial "idle" state. This
231+
* means that you can keep sending additional commands at a later time which
232+
* will again try to open the underlying connection.
226233
*
227234
* Note that creating the underlying connection will be deferred until the
228235
* first request is invoked. Accordingly, any eventual connection issues
229-
* will be detected once this instance is first used. Similarly, calling
230-
* `quit()` on this instance before invoking any requests will succeed
231-
* immediately and will not wait for an actual underlying connection.
236+
* will be detected once this instance is first used. You can use the
237+
* `quit()` method to ensure that the "virtual" connection will be soft-closed
238+
* and no further commands can be enqueued. Similarly, calling `quit()` on
239+
* this instance before invoking any requests will succeed immediately and
240+
* will not wait for an actual underlying connection.
232241
*
233242
* Depending on your particular use case, you may prefer this method or the
234243
* underlying `createConnection()` which resolves with a promise. For many

src/Io/LazyConnection.php

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,22 @@ public function __construct(Factory $factory, $uri)
2727

2828
private function connecting()
2929
{
30-
if ($this->connecting === null) {
31-
$this->connecting = $this->factory->createConnection($this->uri);
32-
33-
$this->connecting->then(function (ConnectionInterface $connection) {
34-
// connection completed => forward error and close events
35-
$connection->on('error', function ($e) {
36-
$this->emit('error', [$e]);
37-
});
38-
$connection->on('close', function () {
39-
$this->close();
40-
});
41-
}, function (\Exception $e) {
42-
// connection failed => emit error if connection is not already closed
43-
if ($this->closed) {
44-
return;
45-
}
30+
if ($this->connecting !== null) {
31+
return $this->connecting;
32+
}
4633

47-
$this->emit('error', [$e]);
48-
$this->close();
34+
$this->connecting = $connecting = $this->factory->createConnection($this->uri);
35+
$this->connecting->then(function (ConnectionInterface $connection) {
36+
// connection completed => remember only until closed
37+
$connection->on('close', function () {
38+
$this->connecting = null;
4939
});
50-
}
40+
}, function () {
41+
// connection failed => discard connection attempt
42+
$this->connecting = null;
43+
});
5144

52-
return $this->connecting;
45+
return $connecting;
5346
}
5447

5548
public function query($sql, array $params = [])
@@ -100,7 +93,15 @@ public function quit()
10093
}
10194

10295
return $this->connecting()->then(function (ConnectionInterface $connection) {
103-
return $connection->quit();
96+
return $connection->quit()->then(
97+
function () {
98+
$this->close();
99+
},
100+
function (\Exception $e) {
101+
$this->close();
102+
throw $e;
103+
}
104+
);
104105
});
105106
}
106107

tests/FactoryTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,18 +387,18 @@ public function testConnectLazyWithValidAuthWillRunUntilQuitAfterPing()
387387
$loop->run();
388388
}
389389

390-
public function testConnectLazyWithInvalidAuthWillEmitErrorAndCloseAfterPing()
390+
public function testConnectLazyWithInvalidAuthWillRejectPingButWillNotEmitErrorOrClose()
391391
{
392392
$loop = \React\EventLoop\Factory::create();
393393
$factory = new Factory($loop);
394394

395395
$uri = $this->getConnectionString(array('passwd' => 'invalidpass'));
396396
$connection = $factory->createLazyConnection($uri);
397397

398-
$connection->on('error', $this->expectCallableOnce());
399-
$connection->on('close', $this->expectCallableOnce());
398+
$connection->on('error', $this->expectCallableNever());
399+
$connection->on('close', $this->expectCallableNever());
400400

401-
$connection->ping();
401+
$connection->ping()->then(null, $this->expectCallableOnce());
402402

403403
$loop->run();
404404
}

tests/Io/LazyConnectionTest.php

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,51 +12,47 @@
1212

1313
class LazyConnectionTest extends BaseTestCase
1414
{
15-
public function testPingWillCloseConnectionWithErrorWhenPendingConnectionFails()
15+
public function testPingWillNotCloseConnectionWhenPendingConnectionFails()
1616
{
1717
$deferred = new Deferred();
1818
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
1919
$factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise());
2020
$connection = new LazyConnection($factory, '');
2121

22-
$connection->on('error', $this->expectCallableOnce());
23-
$connection->on('close', $this->expectCallableOnce());
22+
$connection->on('error', $this->expectCallableNever());
23+
$connection->on('close', $this->expectCallableNever());
2424

2525
$connection->ping();
2626

2727
$deferred->reject(new \RuntimeException());
2828
}
2929

30-
public function testPingWillCloseConnectionWithoutErrorWhenUnderlyingConnectionCloses()
30+
public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses()
3131
{
32-
$promise = new Promise(function () { });
33-
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
34-
$factory->expects($this->once())->method('createConnection')->willReturn($promise);
35-
$base = new LazyConnection($factory, '');
32+
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping'))->disableOriginalConstructor()->getMock();
33+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
3634

3735
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
3836
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
3937
$connection = new LazyConnection($factory, '');
4038

4139
$connection->on('error', $this->expectCallableNever());
42-
$connection->on('close', $this->expectCallableOnce());
40+
$connection->on('close', $this->expectCallableNever());
4341

4442
$connection->ping();
4543
$base->close();
4644
}
4745

48-
public function testPingWillForwardErrorFromUnderlyingConnection()
46+
public function testPingWillNotForwardErrorFromUnderlyingConnection()
4947
{
50-
$promise = new Promise(function () { });
51-
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
52-
$factory->expects($this->once())->method('createConnection')->willReturn($promise);
53-
$base = new LazyConnection($factory, '');
48+
$base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(array('ping'))->disableOriginalConstructor()->getMock();
49+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
5450

5551
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
5652
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
5753
$connection = new LazyConnection($factory, '');
5854

59-
$connection->on('error', $this->expectCallableOnce());
55+
$connection->on('error', $this->expectCallableNever());
6056
$connection->on('close', $this->expectCallableNever());
6157

6258
$connection->ping();
@@ -178,6 +174,33 @@ public function testPingWillPingUnderlyingConnectionWhenResolved()
178174
$connection->ping();
179175
}
180176

177+
public function testPingTwiceWillBothRejectWithSameErrorWhenUnderlyingConnectionRejects()
178+
{
179+
$error = new \RuntimeException();
180+
$deferred = new Deferred();
181+
182+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
183+
$factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise());
184+
$connection = new LazyConnection($factory, '');
185+
186+
$connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error));
187+
$connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error));
188+
189+
$deferred->reject($error);
190+
}
191+
192+
public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingFailedToCreateUnderlyingConnection()
193+
{
194+
$error = new \RuntimeException();
195+
196+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
197+
$factory->expects($this->exactly(2))->method('createConnection')->willReturn(\React\Promise\reject($error));
198+
$connection = new LazyConnection($factory, '');
199+
200+
$connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error));
201+
$connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error));
202+
}
203+
181204
public function testQuitResolvesAndEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending()
182205
{
183206
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
@@ -220,6 +243,45 @@ public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved()
220243
$connection->quit();
221244
}
222245

246+
public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQuits()
247+
{
248+
$base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock();
249+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
250+
$base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve());
251+
252+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
253+
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
254+
$connection = new LazyConnection($factory, '');
255+
256+
$connection->on('close', $this->expectCallableOnce());
257+
258+
$connection->ping();
259+
$ret = $connection->quit();
260+
261+
$this->assertTrue($ret instanceof PromiseInterface);
262+
$ret->then($this->expectCallableOnce(), $this->expectCallableNever());
263+
}
264+
265+
public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFailsToQuit()
266+
{
267+
$error = new \RuntimeException();
268+
$base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock();
269+
$base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve());
270+
$base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject($error));
271+
272+
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();
273+
$factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base));
274+
$connection = new LazyConnection($factory, '');
275+
276+
$connection->on('close', $this->expectCallableOnce());
277+
278+
$connection->ping();
279+
$ret = $connection->quit();
280+
281+
$this->assertTrue($ret instanceof PromiseInterface);
282+
$ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error));
283+
}
284+
223285
public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending()
224286
{
225287
$factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock();

0 commit comments

Comments
 (0)