Skip to content

Commit bd03a1c

Browse files
committed
Merge pull request #51 from clue-labs/timeout
Add TimeoutConnector decorator
2 parents 3bcd121 + 3242509 commit bd03a1c

File tree

4 files changed

+202
-1
lines changed

4 files changed

+202
-1
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,31 @@ $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop,
157157
));
158158
```
159159

160+
### Connection timeouts
161+
162+
The `TimeoutConnector` class decorates any given `Connector` instance.
163+
It provides the same `create()` method, but will automatically reject the
164+
underlying connection attempt if it takes too long.
165+
166+
```php
167+
$timeoutConnector = new React\SocketClient\TimeoutConnector($connector, 3.0, $loop);
168+
169+
$timeoutConnector->create('google.com', 80)->then(function (React\Stream\Stream $stream) {
170+
// connection succeeded within 3.0 seconds
171+
});
172+
```
173+
174+
Pending connection attempts can be cancelled by cancelling its pending promise like so:
175+
176+
```php
177+
$promise = $timeoutConnector->create($host, $port);
178+
179+
$promise->cancel();
180+
```
181+
182+
Calling `cancel()` on a pending promise will cancel the underlying connection
183+
attempt, abort the timer and reject the resulting promise.
184+
160185
### Unix domain sockets
161186

162187
Similarly, the `UnixConnector` class can be used to connect to Unix domain socket (UDS)

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"react/dns": "0.4.*|0.3.*",
99
"react/event-loop": "0.4.*|0.3.*",
1010
"react/stream": "0.4.*|0.3.*",
11-
"react/promise": "^2.1 || ^1.2"
11+
"react/promise": "^2.1 || ^1.2",
12+
"react/promise-timer": "~1.0"
1213
},
1314
"autoload": {
1415
"psr-4": {

src/TimeoutConnector.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace React\SocketClient;
4+
5+
use React\SocketClient\ConnectorInterface;
6+
use React\EventLoop\LoopInterface;
7+
use React\Promise\Timer;
8+
use React\Stream\Stream;
9+
use React\Promise\Promise;
10+
use React\Promise\CancellablePromiseInterface;
11+
12+
class TimeoutConnector implements ConnectorInterface
13+
{
14+
private $connector;
15+
private $timeout;
16+
private $loop;
17+
18+
public function __construct(ConnectorInterface $connector, $timeout, LoopInterface $loop)
19+
{
20+
$this->connector = $connector;
21+
$this->timeout = $timeout;
22+
$this->loop = $loop;
23+
}
24+
25+
public function create($host, $port)
26+
{
27+
$promise = $this->connector->create($host, $port);
28+
29+
return Timer\timeout(new Promise(
30+
function ($resolve, $reject) use ($promise) {
31+
// resolve/reject with result of TCP/IP connection
32+
$promise->then($resolve, $reject);
33+
},
34+
function ($_, $reject) use ($promise) {
35+
// cancellation should reject connection attempt
36+
$reject(new \RuntimeException('Connection attempt cancelled during connection'));
37+
38+
// forefully close TCP/IP connection if it completes despite cancellation
39+
$promise->then(function (Stream $stream) {
40+
$stream->close();
41+
});
42+
43+
// (try to) cancel pending TCP/IP connection
44+
if ($promise instanceof CancellablePromiseInterface) {
45+
$promise->cancel();
46+
}
47+
}
48+
), $this->timeout, $this->loop);
49+
}
50+
}

tests/TimeoutConnectorTest.php

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
namespace React\Tests\SocketClient;
4+
5+
use React\SocketClient\TimeoutConnector;
6+
use React\Promise;
7+
use React\EventLoop\Factory;
8+
9+
class TimeoutConnectorTest extends TestCase
10+
{
11+
public function testRejectsOnTimeout()
12+
{
13+
$promise = new Promise\Promise(function () { });
14+
15+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
16+
$connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise));
17+
18+
$loop = Factory::create();
19+
20+
$timeout = new TimeoutConnector($connector, 0.01, $loop);
21+
22+
$timeout->create('google.com', 80)->then(
23+
$this->expectCallableNever(),
24+
$this->expectCallableOnce()
25+
);
26+
27+
$loop->run();
28+
}
29+
30+
public function testRejectsWhenConnectorRejects()
31+
{
32+
$promise = Promise\reject(new \RuntimeException());
33+
34+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
35+
$connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise));
36+
37+
$loop = Factory::create();
38+
39+
$timeout = new TimeoutConnector($connector, 5.0, $loop);
40+
41+
$timeout->create('google.com', 80)->then(
42+
$this->expectCallableNever(),
43+
$this->expectCallableOnce()
44+
);
45+
46+
$loop->run();
47+
}
48+
49+
public function testResolvesWhenConnectorResolves()
50+
{
51+
$promise = Promise\resolve();
52+
53+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
54+
$connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise));
55+
56+
$loop = Factory::create();
57+
58+
$timeout = new TimeoutConnector($connector, 5.0, $loop);
59+
60+
$timeout->create('google.com', 80)->then(
61+
$this->expectCallableOnce(),
62+
$this->expectCallableNever()
63+
);
64+
65+
$loop->run();
66+
}
67+
68+
public function testRejectsAndCancelsPendingPromiseOnTimeout()
69+
{
70+
$promise = new Promise\Promise(function () { }, $this->expectCallableOnce());
71+
72+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
73+
$connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise));
74+
75+
$loop = Factory::create();
76+
77+
$timeout = new TimeoutConnector($connector, 0.01, $loop);
78+
79+
$timeout->create('google.com', 80)->then(
80+
$this->expectCallableNever(),
81+
$this->expectCallableOnce()
82+
);
83+
84+
$loop->run();
85+
}
86+
87+
public function testCancelsPendingPromiseOnCancel()
88+
{
89+
$promise = new Promise\Promise(function () { }, $this->expectCallableOnce());
90+
91+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
92+
$connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise));
93+
94+
$loop = Factory::create();
95+
96+
$timeout = new TimeoutConnector($connector, 0.01, $loop);
97+
98+
$out = $timeout->create('google.com', 80);
99+
$out->cancel();
100+
101+
$out->then($this->expectCallableNever(), $this->expectCallableOnce());
102+
}
103+
104+
public function testCancelClosesStreamIfTcpResolvesDespiteCancellation()
105+
{
106+
$stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock();
107+
$stream->expects($this->once())->method('close');
108+
109+
$promise = new Promise\Promise(function () { }, function ($resolve) use ($stream) {
110+
$resolve($stream);
111+
});
112+
113+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
114+
$connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise));
115+
116+
$loop = Factory::create();
117+
118+
$timeout = new TimeoutConnector($connector, 0.01, $loop);
119+
120+
$out = $timeout->create('google.com', 80);
121+
$out->cancel();
122+
123+
$out->then($this->expectCallableNever(), $this->expectCallableOnce());
124+
}
125+
}

0 commit comments

Comments
 (0)