Skip to content

Commit cbf0577

Browse files
authored
Merge pull request #88 from clue-labs/idle
Implement "idle" timeout for LazyConnection to close underlying connection when unused and automatically create new underlying connection on demand again
2 parents d0b0c0b + 472ae14 commit cbf0577

File tree

7 files changed

+721
-101
lines changed

7 files changed

+721
-101
lines changed

README.md

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -171,24 +171,35 @@ $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 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.
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 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.
186195

187196
Note that creating the underlying connection will be deferred until the
188197
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.
198+
will be detected once this instance is first used. You can use the
199+
`quit()` method to ensure that the "virtual" connection will be soft-closed
200+
and no further commands can be enqueued. Similarly, calling `quit()` on
201+
this instance when not currently connected will succeed immediately and
202+
will not have to wait for an actual underlying connection.
192203

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

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+
228252
### ConnectionInterface
229253

230254
The `ConnectionInterface` represents a connection that is responsible for
@@ -426,7 +450,7 @@ $connecion->on('close', function () {
426450
});
427451
```
428452

429-
See also the [#close](#close) method.
453+
See also the [`close()`](#close) method.
430454

431455
## Install
432456

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: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -211,24 +211,35 @@ 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 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.
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 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.
226235
*
227236
* Note that creating the underlying connection will be deferred until the
228237
* 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.
238+
* will be detected once this instance is first used. You can use the
239+
* `quit()` method to ensure that the "virtual" connection will be soft-closed
240+
* and no further commands can be enqueued. Similarly, calling `quit()` on
241+
* this instance when not currently connected will succeed immediately and
242+
* will not have to wait for an actual underlying connection.
232243
*
233244
* Depending on your particular use case, you may prefer this method or the
234245
* underlying `createConnection()` which resolves with a promise. For many
@@ -265,11 +276,24 @@ public function createConnection($uri)
265276
* $factory->createLazyConnection('localhost?timeout=0.5');
266277
* ```
267278
*
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+
*
268292
* @param string $uri
269293
* @return ConnectionInterface
270294
*/
271295
public function createLazyConnection($uri)
272296
{
273-
return new LazyConnection($this, $uri);
297+
return new LazyConnection($this, $uri, $this->loop);
274298
}
275299
}

src/Io/LazyConnection.php

Lines changed: 128 additions & 23 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,37 +21,94 @@ class LazyConnection extends EventEmitter implements ConnectionInterface
1921
private $closed = false;
2022
private $busy = false;
2123

22-
public function __construct(Factory $factory, $uri)
24+
/**
25+
* @var ConnectionInterface|null
26+
*/
27+
private $disconnecting;
28+
29+
private $loop;
30+
private $idlePeriod = 60.0;
31+
private $idleTimer;
32+
private $pending = 0;
33+
34+
public function __construct(Factory $factory, $uri, LoopInterface $loop)
2335
{
36+
$args = array();
37+
\parse_str(\parse_url($uri, \PHP_URL_QUERY), $args);
38+
if (isset($args['idle'])) {
39+
$this->idlePeriod = (float)$args['idle'];
40+
}
41+
2442
$this->factory = $factory;
2543
$this->uri = $uri;
44+
$this->loop = $loop;
2645
}
2746

2847
private function connecting()
2948
{
30-
if ($this->connecting === null) {
31-
$this->connecting = $this->factory->createConnection($this->uri);
49+
if ($this->connecting !== null) {
50+
return $this->connecting;
51+
}
3252

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-
}
53+
// force-close connection if still waiting for previous disconnection
54+
if ($this->disconnecting !== null) {
55+
$this->disconnecting->close();
56+
$this->disconnecting = null;
57+
}
4658

47-
$this->emit('error', [$e]);
48-
$this->close();
59+
$this->connecting = $connecting = $this->factory->createConnection($this->uri);
60+
$this->connecting->then(function (ConnectionInterface $connection) {
61+
// connection completed => remember only until closed
62+
$connection->on('close', function () {
63+
$this->connecting = null;
64+
65+
if ($this->idleTimer !== null) {
66+
$this->loop->cancelTimer($this->idleTimer);
67+
$this->idleTimer = null;
68+
}
4969
});
70+
}, function () {
71+
// connection failed => discard connection attempt
72+
$this->connecting = null;
73+
});
74+
75+
return $connecting;
76+
}
77+
78+
private function awake()
79+
{
80+
++$this->pending;
81+
82+
if ($this->idleTimer !== null) {
83+
$this->loop->cancelTimer($this->idleTimer);
84+
$this->idleTimer = null;
5085
}
86+
}
5187

52-
return $this->connecting;
88+
private function idle()
89+
{
90+
--$this->pending;
91+
92+
if ($this->pending < 1 && $this->idlePeriod >= 0) {
93+
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
94+
$this->connecting->then(function (ConnectionInterface $connection) {
95+
$this->disconnecting = $connection;
96+
$connection->quit()->then(
97+
function () {
98+
// successfully disconnected => remove reference
99+
$this->disconnecting = null;
100+
},
101+
function () use ($connection) {
102+
// soft-close failed => force-close connection
103+
$connection->close();
104+
$this->disconnecting = null;
105+
}
106+
);
107+
});
108+
$this->connecting = null;
109+
$this->idleTimer = null;
110+
});
111+
}
53112
}
54113

55114
public function query($sql, array $params = [])
@@ -59,7 +118,17 @@ public function query($sql, array $params = [])
59118
}
60119

61120
return $this->connecting()->then(function (ConnectionInterface $connection) use ($sql, $params) {
62-
return $connection->query($sql, $params);
121+
$this->awake();
122+
return $connection->query($sql, $params)->then(
123+
function (QueryResult $result) {
124+
$this->idle();
125+
return $result;
126+
},
127+
function (\Exception $e) {
128+
$this->idle();
129+
throw $e;
130+
}
131+
);
63132
});
64133
}
65134

@@ -71,7 +140,14 @@ public function queryStream($sql, $params = [])
71140

72141
return \React\Promise\Stream\unwrapReadable(
73142
$this->connecting()->then(function (ConnectionInterface $connection) use ($sql, $params) {
74-
return $connection->queryStream($sql, $params);
143+
$stream = $connection->queryStream($sql, $params);
144+
145+
$this->awake();
146+
$stream->on('close', function () {
147+
$this->idle();
148+
});
149+
150+
return $stream;
75151
})
76152
);
77153
}
@@ -83,7 +159,16 @@ public function ping()
83159
}
84160

85161
return $this->connecting()->then(function (ConnectionInterface $connection) {
86-
return $connection->ping();
162+
$this->awake();
163+
return $connection->ping()->then(
164+
function () {
165+
$this->idle();
166+
},
167+
function (\Exception $e) {
168+
$this->idle();
169+
throw $e;
170+
}
171+
);
87172
});
88173
}
89174

@@ -100,7 +185,16 @@ public function quit()
100185
}
101186

102187
return $this->connecting()->then(function (ConnectionInterface $connection) {
103-
return $connection->quit();
188+
$this->awake();
189+
return $connection->quit()->then(
190+
function () {
191+
$this->close();
192+
},
193+
function (\Exception $e) {
194+
$this->close();
195+
throw $e;
196+
}
197+
);
104198
});
105199
}
106200

@@ -112,6 +206,12 @@ public function close()
112206

113207
$this->closed = true;
114208

209+
// force-close connection if still waiting for previous disconnection
210+
if ($this->disconnecting !== null) {
211+
$this->disconnecting->close();
212+
$this->disconnecting = null;
213+
}
214+
115215
// either close active connection or cancel pending connection attempt
116216
if ($this->connecting !== null) {
117217
$this->connecting->then(function (ConnectionInterface $connection) {
@@ -121,6 +221,11 @@ public function close()
121221
$this->connecting = null;
122222
}
123223

224+
if ($this->idleTimer !== null) {
225+
$this->loop->cancelTimer($this->idleTimer);
226+
$this->idleTimer = null;
227+
}
228+
124229
$this->emit('close');
125230
$this->removeAllListeners();
126231
}

0 commit comments

Comments
 (0)