Skip to content

Commit 228c16c

Browse files
committed
Support cancellation of pending connection attempts
Builds on top of reactphp/socket#170 and reactphp/socket#177 Refs friends-of-reactphp/mysql#84 and clue/reactphp-buzz#110
1 parent 2a37657 commit 228c16c

File tree

4 files changed

+72
-6
lines changed

4 files changed

+72
-6
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ $factory = new Factory($loop, $connector);
100100

101101
#### createClient()
102102

103-
The `createClient($redisUri)` method can be used to create a new [`Client`](#client).
103+
The `createClient($redisUri): PromiseInterface<Client,Exception>` method can be used to
104+
create a new [`Client`](#client).
105+
104106
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
105107
and optionally authenticating (AUTH) and selecting the right database (SELECT).
106108

@@ -115,6 +117,24 @@ $factory->createClient('redis://localhost:6379')->then(
115117
);
116118
```
117119

120+
The method returns a [Promise](https://github.com/reactphp/promise) that
121+
will resolve with a [`Client`](#client)
122+
instance on success or will reject with an `Exception` if the URL is
123+
invalid or the connection or authentication fails.
124+
125+
The returned Promise is implemented in such a way that it can be
126+
cancelled when it is still pending. Cancelling a pending promise will
127+
reject its value with an Exception and will cancel the underlying TCP/IP
128+
connection attempt and/or Redis authentication.
129+
130+
```php
131+
$promise = $factory->createConnection($redisUri);
132+
133+
$loop->addTimer(3.0, function () use ($promise) {
134+
$promise->cancel();
135+
});
136+
```
137+
118138
The `$redisUri` can be given in the
119139
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
120140
`[redis[s]://][:auth@]host[:port][/db]`.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
1717
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
1818
"react/promise": "^2.0 || ^1.1",
19-
"react/socket": "^1.0 || ^0.8.3"
19+
"react/socket": "^1.1"
2020
},
2121
"autoload": {
2222
"psr-4": { "Clue\\React\\Redis\\": "src/" }

src/Factory.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
namespace Clue\React\Redis;
44

5-
use Clue\React\Redis\StreamingClient;
65
use Clue\Redis\Protocol\Factory as ProtocolFactory;
76
use React\EventLoop\LoopInterface;
87
use React\Promise;
8+
use React\Promise\Deferred;
99
use React\Socket\ConnectionInterface;
1010
use React\Socket\Connector;
1111
use React\Socket\ConnectorInterface;
@@ -50,9 +50,20 @@ public function createClient($target)
5050
return Promise\reject($e);
5151
}
5252

53-
$protocol = $this->protocol;
53+
$connecting = $this->connector->connect($parts['authority']);
54+
$deferred = new Deferred(function ($_, $reject) use ($connecting) {
55+
// connection cancelled, start with rejecting attempt, then clean up
56+
$reject(new \RuntimeException('Connection to database server cancelled'));
57+
58+
// either close successful connection or cancel pending connection attempt
59+
$connecting->then(function (ConnectionInterface $connection) {
60+
$connection->close();
61+
});
62+
$connecting->cancel();
63+
});
5464

55-
$promise = $this->connector->connect($parts['authority'])->then(function (ConnectionInterface $stream) use ($protocol) {
65+
$protocol = $this->protocol;
66+
$promise = $connecting->then(function (ConnectionInterface $stream) use ($protocol) {
5667
return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer());
5768
});
5869

@@ -84,7 +95,9 @@ function ($error) use ($client) {
8495
});
8596
}
8697

87-
return $promise;
98+
$promise->then(array($deferred, 'resolve'), array($deferred, 'reject'));
99+
100+
return $deferred->promise();
88101
}
89102

90103
/**

tests/FactoryTest.php

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

55
use Clue\React\Redis\Factory;
66
use React\Promise;
7+
use React\Promise\Deferred;
78

89
class FactoryTest extends TestCase
910
{
@@ -153,4 +154,36 @@ public function testWillRejectIfTargetIsInvalid()
153154

154155
$this->expectPromiseReject($promise);
155156
}
157+
158+
public function testCancelWillRejectPromise()
159+
{
160+
$promise = new \React\Promise\Promise(function () { });
161+
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($promise);
162+
163+
$promise = $this->factory->createClient('redis://127.0.0.1:2');
164+
$promise->cancel();
165+
166+
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
167+
}
168+
169+
public function testCancelWillCancelConnectorWhenConnectionIsPending()
170+
{
171+
$deferred = new Deferred($this->expectCallableOnce());
172+
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($deferred->promise());
173+
174+
$promise = $this->factory->createClient('redis://127.0.0.1:2');
175+
$promise->cancel();
176+
}
177+
178+
public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect()
179+
{
180+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
181+
$stream->expects($this->once())->method('write');
182+
$stream->expects($this->once())->method('close');
183+
184+
$this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream));
185+
186+
$promise = $this->factory->createClient('redis://127.0.0.1:2/123');
187+
$promise->cancel();
188+
}
156189
}

0 commit comments

Comments
 (0)