Skip to content

Commit 64bd30d

Browse files
committed
Implement "idle" timeout to close underlying connection when unused
1 parent 5e4a75c commit 64bd30d

File tree

6 files changed

+267
-17
lines changed

6 files changed

+267
-17
lines changed

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,9 @@ This method immediately returns a "virtual" connection implementing the
215215
Internally, it lazily creates the underlying database connection only on
216216
demand once the first request is invoked on this instance and will queue
217217
all outstanding requests until the underlying connection is ready.
218-
Additionally, it will keep track of this underlying connection and will
219-
create a new underlying connection on demand when the current connection
220-
is lost.
218+
Additionally, it will only keep this underlying connection in an "idle" state
219+
for 60s by default and will automatically close the underlying connection when
220+
it is no longer needed.
221221

222222
From a consumer side this means that you can start sending commands to the
223223
database right away while the underlying connection may still be
@@ -231,7 +231,9 @@ to deal with its async resolution.
231231
If the underlying database connection fails, it will reject all
232232
outstanding commands and will return to the initial "idle" state. This
233233
means that you can keep sending additional commands at a later time which
234-
will again try to open the underlying connection.
234+
will again try to open a new underlying connection. Note that this may
235+
require special care if you're using transactions (`MULTI`/`EXEC`) that are kept
236+
open for longer than the idle period.
235237

236238
If the underlying database connection drops while using PubSub channels
237239
(see `SUBSCRIBE` and `PSUBSCRIBE` commands), it will automatically send the
@@ -245,8 +247,8 @@ first request is invoked. Accordingly, any eventual connection issues
245247
will be detected once this instance is first used. You can use the
246248
`end()` method to ensure that the "virtual" connection will be soft-closed
247249
and no further commands can be enqueued. Similarly, calling `end()` on
248-
this instance before invoking any requests will succeed immediately and
249-
will not wait for an actual underlying connection.
250+
this instance when not currently connected will succeed immediately and
251+
will not have to wait for an actual underlying connection.
250252

251253
Depending on your particular use case, you may prefer this method or the
252254
underlying `createClient()` which resolves with a promise. For many
@@ -312,6 +314,19 @@ in seconds (or use a negative number to not apply a timeout) like this:
312314
$factory->createLazyClient('localhost?timeout=0.5');
313315
```
314316

317+
By default, this method will keep "idle" connection open for 60s and will
318+
then end the underlying connection. The next request after an "idle"
319+
connection ended will automatically create a new underlying connection.
320+
This ensure you always get a "fresh" connection and as such should not be
321+
confused with a "keepalive" or "heartbeat" mechanism, as this will not
322+
actively try to probe the connection. You can explicitly pass a custom
323+
idle timeout value in seconds (or use a negative number to not apply a
324+
timeout) like this:
325+
326+
```php
327+
$factory->createLazyClient('localhost?idle=0.1');
328+
```
329+
315330
### Client
316331

317332
The `Client` is responsible for exchanging messages with Redis

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"php": ">=5.3",
1515
"clue/redis-protocol": "0.3.*",
1616
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
17-
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
17+
"react/event-loop": "^1.0 || ^0.5",
1818
"react/promise": "^2.0 || ^1.1",
1919
"react/promise-timer": "^1.5",
2020
"react/socket": "^1.1"

src/Factory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ function ($error) use ($client) {
123123
*/
124124
public function createLazyClient($target)
125125
{
126-
return new LazyClient($target, $this);
126+
return new LazyClient($target, $this, $this->loop);
127127
}
128128

129129
/**

src/LazyClient.php

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Evenement\EventEmitter;
66
use React\Stream\Util;
7+
use React\EventLoop\LoopInterface;
78

89
/**
910
* @internal
@@ -16,13 +17,25 @@ class LazyClient extends EventEmitter implements Client
1617
private $closed = false;
1718
private $promise;
1819

20+
private $loop;
21+
private $idlePeriod = 60.0;
22+
private $idleTimer;
23+
private $pending = 0;
24+
1925
/**
2026
* @param $target
2127
*/
22-
public function __construct($target, Factory $factory)
28+
public function __construct($target, Factory $factory, LoopInterface $loop)
2329
{
30+
$args = array();
31+
\parse_str(\parse_url($target, \PHP_URL_QUERY), $args);
32+
if (isset($args['idle'])) {
33+
$this->idlePeriod = (float)$args['idle'];
34+
}
35+
2436
$this->target = $target;
2537
$this->factory = $factory;
38+
$this->loop = $loop;
2639
}
2740

2841
private function client()
@@ -33,11 +46,13 @@ private function client()
3346

3447
$self = $this;
3548
$pending =& $this->promise;
36-
return $pending = $this->factory->createClient($this->target)->then(function (Client $client) use ($self, &$pending) {
49+
$idleTimer=& $this->idleTimer;
50+
$loop = $this->loop;
51+
return $pending = $this->factory->createClient($this->target)->then(function (Client $client) use ($self, &$pending, &$idleTimer, $loop) {
3752
// connection completed => remember only until closed
3853
$subscribed = array();
3954
$psubscribed = array();
40-
$client->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed) {
55+
$client->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed, &$idleTimer, $loop) {
4156
$pending = null;
4257

4358
// foward unsubscribe/punsubscribe events when underlying connection closes
@@ -49,6 +64,11 @@ private function client()
4964
foreach ($psubscribed as $pattern => $_) {
5065
$self->emit('punsubscribe', array($pattern, --$n));
5166
}
67+
68+
if ($idleTimer !== null) {
69+
$loop->cancelTimer($idleTimer);
70+
$idleTimer = null;
71+
}
5272
});
5373

5474
// keep track of all channels and patterns this connection is subscribed to
@@ -93,8 +113,19 @@ public function __call($name, $args)
93113
return \React\Promise\reject(new \RuntimeException('Connection closed'));
94114
}
95115

96-
return $this->client()->then(function (Client $client) use ($name, $args) {
97-
return \call_user_func_array(array($client, $name), $args);
116+
$that = $this;
117+
return $this->client()->then(function (Client $client) use ($name, $args, $that) {
118+
$that->awake();
119+
return \call_user_func_array(array($client, $name), $args)->then(
120+
function ($result) use ($that) {
121+
$that->idle();
122+
return $result;
123+
},
124+
function ($error) use ($that) {
125+
$that->idle();
126+
throw $error;
127+
}
128+
);
98129
});
99130
}
100131

@@ -134,7 +165,45 @@ public function close()
134165
$this->promise = null;
135166
}
136167

168+
if ($this->idleTimer !== null) {
169+
$this->loop->cancelTimer($this->idleTimer);
170+
$this->idleTimer = null;
171+
}
172+
137173
$this->emit('close');
138174
$this->removeAllListeners();
139175
}
176+
177+
/**
178+
* @internal
179+
*/
180+
public function awake()
181+
{
182+
++$this->pending;
183+
184+
if ($this->idleTimer !== null) {
185+
$this->loop->cancelTimer($this->idleTimer);
186+
$this->idleTimer = null;
187+
}
188+
}
189+
190+
/**
191+
* @internal
192+
*/
193+
public function idle()
194+
{
195+
--$this->pending;
196+
197+
if ($this->pending < 1 && $this->idlePeriod >= 0) {
198+
$idleTimer =& $this->idleTimer;
199+
$promise =& $this->promise;
200+
$idleTimer = $this->loop->addTimer($this->idlePeriod, function () use (&$idleTimer, &$promise) {
201+
$promise->then(function (Client $client) {
202+
$client->close();
203+
});
204+
$promise = null;
205+
$idleTimer = null;
206+
});
207+
}
208+
}
140209
}

tests/FunctionalTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ public function testPingLazy()
5151
$this->assertEquals('PONG', $ret);
5252
}
5353

54+
/**
55+
* @doesNotPerformAssertions
56+
*/
57+
public function testPingLazyWillNotBlockLoopWhenIdleTimeIsSmall()
58+
{
59+
$client = $this->factory->createLazyClient($this->uri . '?idle=0');
60+
61+
$client->ping();
62+
63+
$this->loop->run();
64+
}
65+
66+
/**
67+
* @doesNotPerformAssertions
68+
*/
69+
public function testLazyClientWithoutCommandsWillNotBlockLoop()
70+
{
71+
$client = $this->factory->createLazyClient($this->uri);
72+
73+
$this->loop->run();
74+
75+
unset($client);
76+
}
77+
5478
public function testMgetIsNotInterpretedAsSubMessage()
5579
{
5680
$client = $this->createClient($this->uri);

0 commit comments

Comments
 (0)