Skip to content

Commit cda663a

Browse files
authored
Merge pull request clue#87 from clue-labs/lazy
Add new createLazyClient() method to connect only on demand and implement "idle" timeout to close underlying connection when unused
2 parents 4e28607 + 917585c commit cda663a

11 files changed

+1109
-59
lines changed

README.md

Lines changed: 149 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ It enables you to set and query its data or use its PubSub topics to react to in
2727
* [Usage](#usage)
2828
* [Factory](#factory)
2929
* [createClient()](#createclient)
30+
* [createLazyClient()](#createlazyclient)
3031
* [Client](#client)
3132
* [Commands](#commands)
3233
* [Promises](#promises)
@@ -46,23 +47,22 @@ local Redis server and send some requests:
4647
$loop = React\EventLoop\Factory::create();
4748
$factory = new Factory($loop);
4849

49-
$factory->createClient('localhost')->then(function (Client $client) use ($loop) {
50-
$client->set('greeting', 'Hello world');
51-
$client->append('greeting', '!');
52-
53-
$client->get('greeting')->then(function ($greeting) {
54-
// Hello world!
55-
echo $greeting . PHP_EOL;
56-
});
57-
58-
$client->incr('invocation')->then(function ($n) {
59-
echo 'This is invocation #' . $n . PHP_EOL;
60-
});
61-
62-
// end connection once all pending requests have been resolved
63-
$client->end();
50+
$client = $factory->createLazyClient('localhost');
51+
$client->set('greeting', 'Hello world');
52+
$client->append('greeting', '!');
53+
54+
$client->get('greeting')->then(function ($greeting) {
55+
// Hello world!
56+
echo $greeting . PHP_EOL;
57+
});
58+
59+
$client->incr('invocation')->then(function ($n) {
60+
echo 'This is invocation #' . $n . PHP_EOL;
6461
});
6562

63+
// end connection once all pending requests have been resolved
64+
$client->end();
65+
6666
$loop->run();
6767
```
6868

@@ -100,7 +100,7 @@ $factory = new Factory($loop, $connector);
100100

101101
#### createClient()
102102

103-
The `createClient($redisUri): PromiseInterface<Client,Exception>` method can be used to
103+
The `createClient(string $redisUri): PromiseInterface<Client,Exception>` method can be used to
104104
create a new [`Client`](#client).
105105

106106
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
@@ -195,6 +195,139 @@ authentication. You can explicitly pass a custom timeout value in seconds
195195
$factory->createClient('localhost?timeout=0.5');
196196
```
197197

198+
#### createLazyClient()
199+
200+
The `createLazyClient(string $redisUri): Client` method can be used to
201+
create a new [`Client`](#client).
202+
203+
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
204+
and optionally authenticating (AUTH) and selecting the right database (SELECT).
205+
206+
```php
207+
$client = $factory->createLazyClient('redis://localhost:6379');
208+
209+
$client->incr('hello');
210+
$client->end();
211+
```
212+
213+
This method immediately returns a "virtual" connection implementing the
214+
[`Client`](#client) that can be used to interface with your Redis database.
215+
Internally, it lazily creates the underlying database connection only on
216+
demand once the first request is invoked on this instance and will queue
217+
all outstanding requests until the underlying connection is ready.
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.
221+
222+
From a consumer side this means that you can start sending commands to the
223+
database right away while the underlying connection may still be
224+
outstanding. Because creating this underlying connection may take some
225+
time, it will enqueue all oustanding commands and will ensure that all
226+
commands will be executed in correct order once the connection is ready.
227+
In other words, this "virtual" connection behaves just like a "real"
228+
connection as described in the `Client` interface and frees you from having
229+
to deal with its async resolution.
230+
231+
If the underlying database connection fails, it will reject all
232+
outstanding commands and will return to the initial "idle" state. This
233+
means that you can keep sending additional commands at a later time which
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.
237+
238+
While using PubSub channels (see `SUBSCRIBE` and `PSUBSCRIBE` commands), this client
239+
will never reach an "idle" state and will keep pending forever (or until the
240+
underlying database connection is lost). Additionally, if the underlying
241+
database connection drops, it will automatically send the appropriate `unsubscribe`
242+
and `punsubscribe` events for all currently active channel and pattern subscriptions.
243+
This allows you to react to these events and restore your subscriptions by
244+
creating a new underlying connection repeating the above commands again.
245+
246+
Note that creating the underlying connection will be deferred until the
247+
first request is invoked. Accordingly, any eventual connection issues
248+
will be detected once this instance is first used. You can use the
249+
`end()` method to ensure that the "virtual" connection will be soft-closed
250+
and no further commands can be enqueued. Similarly, calling `end()` on
251+
this instance when not currently connected will succeed immediately and
252+
will not have to wait for an actual underlying connection.
253+
254+
Depending on your particular use case, you may prefer this method or the
255+
underlying `createClient()` which resolves with a promise. For many
256+
simple use cases it may be easier to create a lazy connection.
257+
258+
The `$redisUri` can be given in the
259+
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
260+
`[redis[s]://][:auth@]host[:port][/db]`.
261+
You can omit the URI scheme and port if you're connecting to the default port 6379:
262+
263+
```php
264+
// both are equivalent due to defaults being applied
265+
$factory->createLazyClient('localhost');
266+
$factory->createLazyClient('redis://localhost:6379');
267+
```
268+
269+
Redis supports password-based authentication (`AUTH` command). Note that Redis'
270+
authentication mechanism does not employ a username, so you can pass the
271+
password `h@llo` URL-encoded (percent-encoded) as part of the URI like this:
272+
273+
```php
274+
// all forms are equivalent
275+
$factory->createLazyClient('redis://:h%40llo@localhost');
276+
$factory->createLazyClient('redis://ignored:h%40llo@localhost');
277+
$factory->createLazyClient('redis://localhost?password=h%40llo');
278+
```
279+
280+
You can optionally include a path that will be used to select (SELECT command) the right database:
281+
282+
```php
283+
// both forms are equivalent
284+
$factory->createLazyClient('redis://localhost/2');
285+
$factory->createLazyClient('redis://localhost?db=2');
286+
```
287+
288+
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
289+
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:
290+
291+
```php
292+
$factory->createLazyClient('rediss://redis.example.com:6340');
293+
```
294+
295+
You can use the `redis+unix://` URI scheme if your Redis instance is listening
296+
on a Unix domain socket (UDS) path:
297+
298+
```php
299+
$factory->createLazyClient('redis+unix:///tmp/redis.sock');
300+
301+
// the URI MAY contain `password` and `db` query parameters as seen above
302+
$factory->createLazyClient('redis+unix:///tmp/redis.sock?password=secret&db=2');
303+
304+
// the URI MAY contain authentication details as userinfo as seen above
305+
// should be used with care, also note that database can not be passed as path
306+
$factory->createLazyClient('redis+unix://:secret@/tmp/redis.sock');
307+
```
308+
309+
This method respects PHP's `default_socket_timeout` setting (default 60s)
310+
as a timeout for establishing the underlying connection and waiting for
311+
successful authentication. You can explicitly pass a custom timeout value
312+
in seconds (or use a negative number to not apply a timeout) like this:
313+
314+
```php
315+
$factory->createLazyClient('localhost?timeout=0.5');
316+
```
317+
318+
By default, this method will keep "idle" connection open for 60s and will
319+
then end the underlying connection. The next request after an "idle"
320+
connection ended will automatically create a new underlying connection.
321+
This ensure you always get a "fresh" connection and as such should not be
322+
confused with a "keepalive" or "heartbeat" mechanism, as this will not
323+
actively try to probe the connection. You can explicitly pass a custom
324+
idle timeout value in seconds (or use a negative number to not apply a
325+
timeout) like this:
326+
327+
```php
328+
$factory->createLazyClient('localhost?idle=0.1');
329+
```
330+
198331
### Client
199332

200333
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"

examples/incr.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
<?php
22

3-
use Clue\React\Redis\Client;
43
use Clue\React\Redis\Factory;
54

65
require __DIR__ . '/../vendor/autoload.php';
76

87
$loop = React\EventLoop\Factory::create();
98
$factory = new Factory($loop);
109

11-
$factory->createClient('localhost')->then(function (Client $client) {
12-
$client->incr('test');
10+
$client = $factory->createLazyClient('localhost');
11+
$client->incr('test');
1312

14-
$client->get('test')->then(function ($result) {
15-
var_dump($result);
16-
});
17-
18-
$client->end();
13+
$client->get('test')->then(function ($result) {
14+
var_dump($result);
1915
});
2016

17+
$client->end();
18+
2119
$loop->run();

examples/publish.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?php
22

3-
use Clue\React\Redis\Client;
43
use Clue\React\Redis\Factory;
54

65
require __DIR__ . '/../vendor/autoload.php';
@@ -11,12 +10,11 @@
1110
$channel = isset($argv[1]) ? $argv[1] : 'channel';
1211
$message = isset($argv[2]) ? $argv[2] : 'message';
1312

14-
$factory->createClient('localhost')->then(function (Client $client) use ($channel, $message) {
15-
$client->publish($channel, $message)->then(function ($received) {
16-
echo 'successfully published. Received by ' . $received . PHP_EOL;
17-
});
18-
19-
$client->end();
13+
$client = $factory->createLazyClient('localhost');
14+
$client->publish($channel, $message)->then(function ($received) {
15+
echo 'successfully published. Received by ' . $received . PHP_EOL;
2016
});
2117

18+
$client->end();
19+
2220
$loop->run();

examples/subscribe.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?php
22

3-
use Clue\React\Redis\Client;
43
use Clue\React\Redis\Factory;
54

65
require __DIR__ . '/../vendor/autoload.php';
@@ -10,13 +9,28 @@
109

1110
$channel = isset($argv[1]) ? $argv[1] : 'channel';
1211

13-
$factory->createClient('localhost')->then(function (Client $client) use ($channel) {
14-
$client->subscribe($channel)->then(function () {
15-
echo 'Now subscribed to channel ' . PHP_EOL;
16-
});
12+
$client = $factory->createLazyClient('localhost');
13+
$client->subscribe($channel)->then(function () {
14+
echo 'Now subscribed to channel ' . PHP_EOL;
15+
}, function (Exception $e) {
16+
echo 'Unable to subscribe: ' . $e->getMessage() . PHP_EOL;
17+
});
18+
19+
$client->on('message', function ($channel, $message) {
20+
echo 'Message on ' . $channel . ': ' . $message . PHP_EOL;
21+
});
22+
23+
// automatically re-subscribe to channel on connection issues
24+
$client->on('unsubscribe', function ($channel) use ($client, $loop) {
25+
echo 'Unsubscribed from ' . $channel . PHP_EOL;
1726

18-
$client->on('message', function ($channel, $message) {
19-
echo 'Message on ' . $channel . ': ' . $message . PHP_EOL;
27+
$loop->addPeriodicTimer(2.0, function ($timer) use ($client, $channel, $loop){
28+
$client->subscribe($channel)->then(function () use ($timer, $loop) {
29+
echo 'Now subscribed again' . PHP_EOL;
30+
$loop->cancelTimer($timer);
31+
}, function (Exception $e) {
32+
echo 'Unable to subscribe again: ' . $e->getMessage() . PHP_EOL;
33+
});
2034
});
2135
});
2236

src/Factory.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector =
3939
}
4040

4141
/**
42-
* create redis client connected to address of given redis instance
42+
* Create Redis client connected to address of given redis instance
4343
*
4444
* @param string $target Redis server URI to connect to
45-
* @return \React\Promise\PromiseInterface resolves with Client or rejects with \Exception
45+
* @return \React\Promise\PromiseInterface<Client> resolves with Client or rejects with \Exception
4646
*/
4747
public function createClient($target)
4848
{
@@ -115,6 +115,17 @@ function ($error) use ($client) {
115115
});
116116
}
117117

118+
/**
119+
* Create Redis client connected to address of given redis instance
120+
*
121+
* @param string $target
122+
* @return Client
123+
*/
124+
public function createLazyClient($target)
125+
{
126+
return new LazyClient($target, $this, $this->loop);
127+
}
128+
118129
/**
119130
* @param string $target
120131
* @return array with keys authority, auth and db

0 commit comments

Comments
 (0)