Skip to content

Commit 160b3f2

Browse files
committed
Implement "idle" timeout to close underlying connection when unused
1 parent 740e97a commit 160b3f2

File tree

7 files changed

+457
-54
lines changed

7 files changed

+457
-54
lines changed

README.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ This method immediately returns a "virtual" connection implementing the
173173
interface with your MySQL database. Internally, it lazily creates the
174174
underlying database connection only on demand once the first request is
175175
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.
176+
the underlying connection is ready. Additionally, it will only keep this
177+
underlying connection in an "idle" state for 60s by default and will
178+
automatically end the underlying connection when it is no longer needed.
179179

180180
From a consumer side this means that you can start sending queries to the
181181
database right away while the underlying connection may still be
@@ -189,15 +189,17 @@ having to deal with its async resolution.
189189
If the underlying database connection fails, it will reject all
190190
outstanding commands and will return to the initial "idle" state. This
191191
means that you can keep sending additional commands at a later time which
192-
will again try to open the underlying connection.
192+
will again try to open a new underlying connection. Note that this may
193+
require special care if you're using transactions that are kept open for
194+
longer than the idle period.
193195

194196
Note that creating the underlying connection will be deferred until the
195197
first request is invoked. Accordingly, any eventual connection issues
196198
will be detected once this instance is first used. You can use the
197199
`quit()` method to ensure that the "virtual" connection will be soft-closed
198200
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.
201+
this instance when not currently connected will succeed immediately and
202+
will not have to wait for an actual underlying connection.
201203

202204
Depending on your particular use case, you may prefer this method or the
203205
underlying `createConnection()` which resolves with a promise. For many
@@ -234,6 +236,19 @@ in seconds (or use a negative number to not apply a timeout) like this:
234236
$factory->createLazyConnection('localhost?timeout=0.5');
235237
```
236238

239+
By default, this method will keep "idle" connection open for 60s and will
240+
then end the underlying connection. The next request after an "idle"
241+
connection ended will automatically create a new underlying connection.
242+
This ensure you always get a "fresh" connection and as such should not be
243+
confused with a "keepalive" or "heartbeat" mechanism, as this will not
244+
actively try to probe the connection. You can explicitly pass a custom
245+
idle timeout value in seconds (or use a negative number to not apply a
246+
timeout) like this:
247+
248+
```php
249+
$factory->createLazyConnection('localhost?idle=0.1');
250+
```
251+
237252
### ConnectionInterface
238253

239254
The `ConnectionInterface` represents a connection that is responsible for
@@ -435,7 +450,7 @@ $connecion->on('close', function () {
435450
});
436451
```
437452

438-
See also the [#close](#close) method.
453+
See also the [`close()`](#close) method.
439454

440455
## Install
441456

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"require": {
77
"php": ">=5.4.0",
88
"evenement/evenement": "^3.0 || ^2.1 || ^1.1",
9-
"react/event-loop": "^1.0 || ^0.5 || ^0.4",
9+
"react/event-loop": "^1.0 || ^0.5",
1010
"react/promise": "^2.7",
1111
"react/promise-stream": "^1.1",
1212
"react/promise-timer": "^1.5",

src/ConnectionInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
* });
3939
* ```
4040
*
41-
* See also the [#close](#close) method.
41+
* See also the [`close()`](#close) method.
4242
*/
4343
interface ConnectionInterface extends EventEmitterInterface
4444
{

src/Factory.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,9 @@ public function createConnection($uri)
213213
* interface with your MySQL database. Internally, it lazily creates the
214214
* underlying database connection only on demand once the first request is
215215
* 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.
216+
* the underlying connection is ready. Additionally, it will only keep this
217+
* underlying connection in an "idle" state for 60s by default and will
218+
* automatically end the underlying connection when it is no longer needed.
219219
*
220220
* From a consumer side this means that you can start sending queries to the
221221
* database right away while the underlying connection may still be
@@ -229,15 +229,17 @@ public function createConnection($uri)
229229
* If the underlying database connection fails, it will reject all
230230
* outstanding commands and will return to the initial "idle" state. This
231231
* means that you can keep sending additional commands at a later time which
232-
* will again try to open the underlying connection.
232+
* will again try to open a new underlying connection. Note that this may
233+
* require special care if you're using transactions that are kept open for
234+
* longer than the idle period.
233235
*
234236
* Note that creating the underlying connection will be deferred until the
235237
* first request is invoked. Accordingly, any eventual connection issues
236238
* will be detected once this instance is first used. You can use the
237239
* `quit()` method to ensure that the "virtual" connection will be soft-closed
238240
* 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.
241+
* this instance when not currently connected will succeed immediately and
242+
* will not have to wait for an actual underlying connection.
241243
*
242244
* Depending on your particular use case, you may prefer this method or the
243245
* underlying `createConnection()` which resolves with a promise. For many
@@ -274,11 +276,24 @@ public function createConnection($uri)
274276
* $factory->createLazyConnection('localhost?timeout=0.5');
275277
* ```
276278
*
279+
* By default, this method will keep "idle" connection open for 60s and will
280+
* then end the underlying connection. The next request after an "idle"
281+
* connection ended will automatically create a new underlying connection.
282+
* This ensure you always get a "fresh" connection and as such should not be
283+
* confused with a "keepalive" or "heartbeat" mechanism, as this will not
284+
* actively try to probe the connection. You can explicitly pass a custom
285+
* idle timeout value in seconds (or use a negative number to not apply a
286+
* timeout) like this:
287+
*
288+
* ```php
289+
* $factory->createLazyConnection('localhost?idle=0.1');
290+
* ```
291+
*
277292
* @param string $uri
278293
* @return ConnectionInterface
279294
*/
280295
public function createLazyConnection($uri)
281296
{
282-
return new LazyConnection($this, $uri);
297+
return new LazyConnection($this, $uri, $this->loop);
283298
}
284299
}

src/Io/LazyConnection.php

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Evenement\EventEmitter;
77
use React\MySQL\Exception;
88
use React\MySQL\Factory;
9+
use React\EventLoop\LoopInterface;
10+
use React\MySQL\QueryResult;
911

1012
/**
1113
* @internal
@@ -19,10 +21,22 @@ class LazyConnection extends EventEmitter implements ConnectionInterface
1921
private $closed = false;
2022
private $busy = false;
2123

22-
public function __construct(Factory $factory, $uri)
24+
private $loop;
25+
private $idlePeriod = 60.0;
26+
private $idleTimer;
27+
private $pending = 0;
28+
29+
public function __construct(Factory $factory, $uri, LoopInterface $loop)
2330
{
31+
$args = array();
32+
\parse_str(\parse_url($uri, \PHP_URL_QUERY), $args);
33+
if (isset($args['idle'])) {
34+
$this->idlePeriod = (float)$args['idle'];
35+
}
36+
2437
$this->factory = $factory;
2538
$this->uri = $uri;
39+
$this->loop = $loop;
2640
}
2741

2842
private function connecting()
@@ -36,6 +50,11 @@ private function connecting()
3650
// connection completed => remember only until closed
3751
$connection->on('close', function () {
3852
$this->connecting = null;
53+
54+
if ($this->idleTimer !== null) {
55+
$this->loop->cancelTimer($this->idleTimer);
56+
$this->idleTimer = null;
57+
}
3958
});
4059
}, function () {
4160
// connection failed => discard connection attempt
@@ -45,14 +64,49 @@ private function connecting()
4564
return $connecting;
4665
}
4766

67+
private function awake()
68+
{
69+
++$this->pending;
70+
71+
if ($this->idleTimer !== null) {
72+
$this->loop->cancelTimer($this->idleTimer);
73+
$this->idleTimer = null;
74+
}
75+
}
76+
77+
private function idle()
78+
{
79+
--$this->pending;
80+
81+
if ($this->pending < 1 && $this->idlePeriod >= 0) {
82+
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
83+
$this->connecting->then(function (ConnectionInterface $connection) {
84+
$connection->quit();
85+
});
86+
$this->connecting = null;
87+
$this->idleTimer = null;
88+
});
89+
}
90+
}
91+
4892
public function query($sql, array $params = [])
4993
{
5094
if ($this->closed) {
5195
return \React\Promise\reject(new Exception('Connection closed'));
5296
}
5397

5498
return $this->connecting()->then(function (ConnectionInterface $connection) use ($sql, $params) {
55-
return $connection->query($sql, $params);
99+
$this->awake();
100+
return $connection->query($sql, $params)->then(
101+
function (QueryResult $result) {
102+
$this->idle();
103+
return $result;
104+
},
105+
function (\Exception $e) {
106+
$this->idle();
107+
throw $e;
108+
}
109+
);
56110
});
57111
}
58112

@@ -64,7 +118,14 @@ public function queryStream($sql, $params = [])
64118

65119
return \React\Promise\Stream\unwrapReadable(
66120
$this->connecting()->then(function (ConnectionInterface $connection) use ($sql, $params) {
67-
return $connection->queryStream($sql, $params);
121+
$stream = $connection->queryStream($sql, $params);
122+
123+
$this->awake();
124+
$stream->on('close', function () {
125+
$this->idle();
126+
});
127+
128+
return $stream;
68129
})
69130
);
70131
}
@@ -76,7 +137,16 @@ public function ping()
76137
}
77138

78139
return $this->connecting()->then(function (ConnectionInterface $connection) {
79-
return $connection->ping();
140+
$this->awake();
141+
return $connection->ping()->then(
142+
function () {
143+
$this->idle();
144+
},
145+
function (\Exception $e) {
146+
$this->idle();
147+
throw $e;
148+
}
149+
);
80150
});
81151
}
82152

@@ -93,6 +163,7 @@ public function quit()
93163
}
94164

95165
return $this->connecting()->then(function (ConnectionInterface $connection) {
166+
$this->awake();
96167
return $connection->quit()->then(
97168
function () {
98169
$this->close();
@@ -122,6 +193,11 @@ public function close()
122193
$this->connecting = null;
123194
}
124195

196+
if ($this->idleTimer !== null) {
197+
$this->loop->cancelTimer($this->idleTimer);
198+
$this->idleTimer = null;
199+
}
200+
125201
$this->emit('close');
126202
$this->removeAllListeners();
127203
}

tests/FactoryTest.php

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

390+
public function testConnectLazyWithValidAuthWillRunUntilIdleTimerAfterPingEvenWithoutQuit()
391+
{
392+
$loop = \React\EventLoop\Factory::create();
393+
$factory = new Factory($loop);
394+
395+
$uri = $this->getConnectionString() . '?idle=0';
396+
$connection = $factory->createLazyConnection($uri);
397+
398+
$connection->ping();
399+
400+
$loop->run();
401+
}
402+
390403
public function testConnectLazyWithInvalidAuthWillRejectPingButWillNotEmitErrorOrClose()
391404
{
392405
$loop = \React\EventLoop\Factory::create();

0 commit comments

Comments
 (0)