Skip to content

Commit 82dc4c7

Browse files
committed
Make sure lazy client follows exactly Client semantics
1 parent b35b2ed commit 82dc4c7

File tree

11 files changed

+420
-310
lines changed

11 files changed

+420
-310
lines changed

README.md

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,22 @@ local Redis server and send some requests:
4747
$loop = React\EventLoop\Factory::create();
4848
$factory = new Factory($loop);
4949

50-
$factory->createClient('localhost')->then(function (Client $client) use ($loop) {
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;
61-
});
62-
63-
// end connection once all pending requests have been resolved
64-
$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;
6561
});
6662

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

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

102101
#### createClient()
103102

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

107106
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
@@ -198,9 +197,103 @@ $factory->createClient('localhost?timeout=0.5');
198197

199198
#### createLazyClient()
200199

201-
The `createLazyClient($redisUri)` method can be used to create a new [`Client`](#client) which lazily
202-
creates and connects to the configured redis server on the first command. Internally it will use `createClient()`
203-
when the first command comes in, queues all commands while connecting, and pass on all commands directly when connected.
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 (which may
216+
take some time) only once the first request is invoked on this instance and
217+
will queue all outstanding requests until the underlying connection is ready.
218+
219+
From a consumer side this means that you can start sending commands to the
220+
database right away while the actual connection may still be outstanding.
221+
It will ensure that all commands will be executed in the order they are
222+
enqueued once the connection is ready. If the database connection fails,
223+
it will emit an `error` event, reject all outstanding commands and `close`
224+
the connection as described in the `Client`. In other words, it behaves just
225+
like a real connection and frees you from having to deal with its async
226+
resolution.
227+
228+
Note that creating the underlying connection will be deferred until the
229+
first request is invoked. Accordingly, any eventual connection issues
230+
will be detected once this instance is first used. Similarly, calling
231+
`end()` on this instance before invoking any requests will succeed
232+
immediately and will not wait for an actual underlying connection.
233+
234+
Depending on your particular use case, you may prefer this method or the
235+
underlying `createClient()` which resolves with a promise. For many
236+
simple use cases it may be easier to create a lazy connection.
237+
238+
The `$redisUri` can be given in the
239+
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
240+
`[redis[s]://][:auth@]host[:port][/db]`.
241+
You can omit the URI scheme and port if you're connecting to the default port 6379:
242+
243+
```php
244+
// both are equivalent due to defaults being applied
245+
$factory->createLazyClient('localhost');
246+
$factory->createLazyClient('redis://localhost:6379');
247+
```
248+
249+
Redis supports password-based authentication (`AUTH` command). Note that Redis'
250+
authentication mechanism does not employ a username, so you can pass the
251+
password `h@llo` URL-encoded (percent-encoded) as part of the URI like this:
252+
253+
```php
254+
// all forms are equivalent
255+
$factory->createLazyClient('redis://:h%40llo@localhost');
256+
$factory->createLazyClient('redis://ignored:h%40llo@localhost');
257+
$factory->createLazyClient('redis://localhost?password=h%40llo');
258+
```
259+
260+
You can optionally include a path that will be used to select (SELECT command) the right database:
261+
262+
```php
263+
// both forms are equivalent
264+
$factory->createLazyClient('redis://localhost/2');
265+
$factory->createLazyClient('redis://localhost?db=2');
266+
```
267+
268+
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
269+
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:
270+
271+
```php
272+
$factory->createLazyClient('rediss://redis.example.com:6340');
273+
```
274+
275+
You can use the `redis+unix://` URI scheme if your Redis instance is listening
276+
on a Unix domain socket (UDS) path:
277+
278+
```php
279+
$factory->createLazyClient('redis+unix:///tmp/redis.sock');
280+
281+
// the URI MAY contain `password` and `db` query parameters as seen above
282+
$factory->createLazyClient('redis+unix:///tmp/redis.sock?password=secret&db=2');
283+
284+
// the URI MAY contain authentication details as userinfo as seen above
285+
// should be used with care, also note that database can not be passed as path
286+
$factory->createLazyClient('redis+unix://:secret@/tmp/redis.sock');
287+
```
288+
289+
This method respects PHP's `default_socket_timeout` setting (default 60s)
290+
as a timeout for establishing the underlying connection and waiting for
291+
successful authentication. You can explicitly pass a custom timeout value
292+
in seconds (or use a negative number to not apply a timeout) like this:
293+
294+
```php
295+
$factory->createLazyClient('localhost?timeout=0.5');
296+
```
204297

205298
### Client
206299

examples/cli.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111

1212
echo '# connecting to redis...' . PHP_EOL;
1313

14-
/** @var Client $client */
15-
$client = $factory->createLazyClient('localhost');
16-
17-
try {
14+
$factory->createClient('localhost')->then(function (Client $client) use ($loop) {
1815
echo '# connected! Entering interactive mode, hit CTRL-D to quit' . PHP_EOL;
1916

2017
$loop->addReadStream(STDIN, function () use ($client, $loop) {
@@ -51,10 +48,10 @@
5148

5249
$loop->removeReadStream(STDIN);
5350
});
54-
} catch (Exception $error) {
51+
}, function (Exception $error) {
5552
echo 'CONNECTION ERROR: ' . $error->getMessage() . PHP_EOL;
5653
exit(1);
57-
};
54+
});
5855

5956

6057
$loop->run();

examples/incr.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
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-
/** @var Client $client */
1210
$client = $factory->createLazyClient('localhost');
1311
$client->incr('test');
1412

examples/publish.php

Lines changed: 0 additions & 2 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,7 +10,6 @@
1110
$channel = isset($argv[1]) ? $argv[1] : 'channel';
1211
$message = isset($argv[2]) ? $argv[2] : 'message';
1312

14-
/** @var Client $client */
1513
$client = $factory->createLazyClient('localhost');
1614
$client->publish($channel, $message)->then(function ($received) {
1715
echo 'successfully published. Received by ' . $received . PHP_EOL;

examples/subscribe.php

Lines changed: 0 additions & 2 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,7 +9,6 @@
109

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

13-
/** @var Client $client */
1412
$client = $factory->createLazyClient('localhost');
1513
$client->subscribe($channel)->then(function () {
1614
echo 'Now subscribed to channel ' . PHP_EOL;

src/Factory.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,11 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector =
3838
$this->protocol = $protocol;
3939
}
4040

41-
public function createLazyClient($target)
42-
{
43-
return new LazyStreamingClient($target, $this);
44-
}
45-
4641
/**
47-
* create redis client connected to address of given redis instance
42+
* Create Redis client connected to address of given redis instance
4843
*
4944
* @param string $target Redis server URI to connect to
50-
* @return \React\Promise\PromiseInterface resolves with Client or rejects with \Exception
45+
* @return \React\Promise\PromiseInterface<Client> resolves with Client or rejects with \Exception
5146
*/
5247
public function createClient($target)
5348
{
@@ -120,6 +115,17 @@ function ($error) use ($client) {
120115
});
121116
}
122117

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);
127+
}
128+
123129
/**
124130
* @param string $target
125131
* @return array with keys authority, auth and db

src/LazyStreamingClient.php renamed to src/LazyClient.php

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,20 @@
33
namespace Clue\React\Redis;
44

55
use Evenement\EventEmitter;
6-
use Clue\Redis\Protocol\Parser\ParserInterface;
7-
use Clue\Redis\Protocol\Parser\ParserException;
8-
use Clue\Redis\Protocol\Serializer\SerializerInterface;
9-
use Clue\Redis\Protocol\Factory as ProtocolFactory;
10-
use React\Promise\FulfilledPromise;
11-
use React\Promise\Promise;
126
use React\Promise\PromiseInterface;
137
use React\Stream\Util;
14-
use UnderflowException;
15-
use RuntimeException;
16-
use InvalidArgumentException;
17-
use React\Promise\Deferred;
18-
use Clue\Redis\Protocol\Model\ErrorReply;
19-
use Clue\Redis\Protocol\Model\ModelInterface;
20-
use Clue\Redis\Protocol\Model\MultiBulkReply;
21-
use React\Stream\DuplexStreamInterface;
228

239
/**
2410
* @internal
2511
*/
26-
class LazyStreamingClient extends EventEmitter implements Client
12+
class LazyClient extends EventEmitter implements Client
2713
{
2814
private $target;
2915
/** @var Factory */
3016
private $factory;
3117
private $ending = false;
3218
private $closed = false;
33-
public $promise = null;
34-
public $client = null;
19+
private $promise;
3520

3621
/**
3722
* @param $target
@@ -50,21 +35,13 @@ private function client()
5035
return $this->promise;
5136
}
5237

53-
if ($this->client instanceof Client) {
54-
return new FulfilledPromise($this->client());
55-
}
56-
5738
$self = $this;
5839
return $this->promise = $this->factory->createClient($this->target)->then(function (Client $client) use ($self) {
59-
$self->client = $client;
60-
$self->promise = null;
61-
6240
Util::forwardEvents(
63-
$self->client,
41+
$client,
6442
$self,
6543
array(
6644
'error',
67-
'close',
6845
'message',
6946
'subscribe',
7047
'unsubscribe',
@@ -74,6 +51,8 @@ private function client()
7451
)
7552
);
7653

54+
$client->on('close', array($self, 'close'));
55+
7756
return $client;
7857
}, function (\Exception $e) use ($self) {
7958
// connection failed => emit error if connection is not already closed
@@ -83,14 +62,14 @@ private function client()
8362
$self->emit('error', array($e));
8463
$self->close();
8564

86-
return $e;
65+
throw $e;
8766
});
8867
}
8968

9069
public function __call($name, $args)
9170
{
92-
if ($this->client instanceof Client) {
93-
return \call_user_func_array(array($this->client, $name), $args);
71+
if ($this->closed) {
72+
return \React\Promise\reject(new \RuntimeException('Connection closed'));
9473
}
9574

9675
return $this->client()->then(function (Client $client) use ($name, $args) {
@@ -100,23 +79,37 @@ public function __call($name, $args)
10079

10180
public function end()
10281
{
103-
if ($this->client instanceof Client) {
104-
return $this->client->end();
82+
if ($this->promise === null) {
83+
$this->close();
84+
}
85+
86+
if ($this->closed) {
87+
return;
10588
}
10689

10790
return $this->client()->then(function (Client $client) {
108-
return $client->end();
91+
$client->end();
10992
});
11093
}
11194

11295
public function close()
11396
{
114-
if ($this->client instanceof Client) {
115-
return $this->client->close();
97+
if ($this->closed) {
98+
return;
11699
}
117100

118-
return $this->client()->then(function (Client $client) {
119-
return $client->close();
120-
});
101+
$this->closed = true;
102+
103+
// either close active connection or cancel pending connection attempt
104+
if ($this->promise !== null) {
105+
$this->promise->then(function (Client $client) {
106+
$client->close();
107+
});
108+
$this->promise->cancel();
109+
$this->promise = null;
110+
}
111+
112+
$this->emit('close');
113+
$this->removeAllListeners();
121114
}
122115
}

0 commit comments

Comments
 (0)