Skip to content

Commit b5a5c55

Browse files
committed
Support Promise cancellation for all connectors
1 parent e585eb8 commit b5a5c55

8 files changed

+197
-19
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"php": ">=5.3",
1818
"react/socket-client": "^0.5 || ^0.4 || ^0.3",
1919
"react/event-loop": "^0.4 || ^0.3",
20-
"react/promise": "^2.0 || ^1.1",
20+
"react/promise": "^2.1 || ^1.2",
2121
"react/promise-timer": "^1.1"
2222
}
2323
}

src/ConnectionManagerRepeat.php

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use React\SocketClient\ConnectorInterface;
66
use InvalidArgumentException;
77
use Exception;
8+
use React\Promise\Promise;
9+
use React\Promise\CancellablePromiseInterface;
810

911
class ConnectionManagerRepeat implements ConnectorInterface
1012
{
@@ -27,16 +29,29 @@ public function create($host, $port)
2729

2830
public function tryConnection($repeat, $host, $port)
2931
{
30-
$that = $this;
31-
return $this->connectionManager->create($host, $port)->then(
32-
null,
33-
function ($error) use ($repeat, $that, $host, $port) {
34-
if ($repeat > 0) {
35-
return $that->tryConnection($repeat - 1, $host, $port);
32+
$tries = $repeat + 1;
33+
$connector = $this->connectionManager;
34+
35+
return new Promise(function ($resolve, $reject) use ($host, $port, &$pending, &$tries, $connector) {
36+
$try = function ($error = null) use (&$try, &$pending, &$tries, $host, $port, $connector, $resolve, $reject) {
37+
if ($tries > 0) {
38+
--$tries;
39+
$pending = $connector->create($host, $port);
40+
$pending->then($resolve, $try);
3641
} else {
37-
throw new Exception('Connection still fails even after repeating', 0, $error);
42+
$reject(new Exception('Connection still fails even after repeating', 0, $error));
3843
}
44+
};
45+
46+
$try();
47+
}, function ($_, $reject) use (&$pending, &$tries) {
48+
// stop retrying, reject results and cancel pending attempt
49+
$tries = 0;
50+
$reject(new \RuntimeException('Cancelled'));
51+
52+
if ($pending instanceof CancellablePromiseInterface) {
53+
$pending->cancel();
3954
}
40-
);
55+
});
4156
}
4257
}

src/Multiple/ConnectionManagerConsecutive.php

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use React\SocketClient\ConnectorInterface;
66
use React\Promise;
77
use UnderflowException;
8+
use React\Promise\CancellablePromiseInterface;
89

910
class ConnectionManagerConsecutive implements ConnectorInterface
1011
{
@@ -30,14 +31,26 @@ public function create($host, $port)
3031
*/
3132
public function tryConnection(array $managers, $host, $port)
3233
{
33-
if (!$managers) {
34-
return Promise\reject(new UnderflowException('No more managers to try to connect through'));
35-
}
36-
$manager = array_shift($managers);
37-
$that = $this;
38-
return $manager->create($host,$port)->then(null, function() use ($that, $managers, $host, $port) {
39-
// connection failed, re-try with remaining connection managers
40-
return $that->tryConnection($managers, $host, $port);
34+
return new Promise\Promise(function ($resolve, $reject) use (&$managers, &$pending, $host, $port) {
35+
$try = function () use (&$try, &$managers, $host, $port, $resolve, $reject, &$pending) {
36+
if (!$managers) {
37+
return $reject(new UnderflowException('No more managers to try to connect through'));
38+
}
39+
40+
$manager = array_shift($managers);
41+
$pending = $manager->create($host, $port);
42+
$pending->then($resolve, $try);
43+
};
44+
45+
$try();
46+
}, function ($_, $reject) use (&$managers, &$pending) {
47+
// stop retrying, reject results and cancel pending attempt
48+
$managers = array();
49+
$reject(new \RuntimeException('Cancelled'));
50+
51+
if ($pending instanceof CancellablePromiseInterface) {
52+
$pending->cancel();
53+
}
4154
});
4255
}
4356
}

tests/ConnectionManagerDelayTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
class ConnectionManagerDelayTest extends TestCase
77
{
8+
private $loop;
9+
810
public function setUp()
911
{
1012
$this->loop = React\EventLoop\Factory::create();
@@ -21,4 +23,17 @@ public function testDelayTenth()
2123
$this->loop->run();
2224
$promise->then($this->expectCallableOnce(), $this->expectCallableNever());
2325
}
26+
27+
public function testCancellationOfPromiseBeforeDelayDoesNotStartConnection()
28+
{
29+
$unused = $this->getMock('React\SocketClient\ConnectorInterface');
30+
$unused->expects($this->never())->method('create');
31+
32+
$cm = new ConnectionManagerDelay($unused, $this->loop, 1.0);
33+
34+
$promise = $cm->create('www.google.com', 80);
35+
$promise->cancel();
36+
37+
$this->loop->run();
38+
}
2439
}

tests/ConnectionManagerRepeatTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,32 @@ public function testInvalidRepetitions()
3939
$wont = new ConnectionManagerReject();
4040
$cm = new ConnectionManagerRepeat($wont, -3);
4141
}
42+
43+
public function testCancellationWillNotStartAnyFurtherConnections()
44+
{
45+
$pending = new Promise\Promise(function () { }, $this->expectCallableOnce());
46+
47+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
48+
$connector->expects($this->once())->method('create')->with('google.com', 80)->willReturn($pending);
49+
50+
$cm = new ConnectionManagerRepeat($connector, 3);
51+
52+
$promise = $cm->create('google.com', 80);
53+
$promise->cancel();
54+
}
55+
56+
public function testCancellationWillNotStartAnyFurtherConnectionsIfPromiseRejectsOnCancellation()
57+
{
58+
$pending = new Promise\Promise(function () { }, function () {
59+
throw new \RuntimeException('cancelled');
60+
});
61+
62+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
63+
$connector->expects($this->once())->method('create')->with('google.com', 80)->willReturn($pending);
64+
65+
$cm = new ConnectionManagerRepeat($connector, 3);
66+
67+
$promise = $cm->create('google.com', 80);
68+
$promise->cancel();
69+
}
4270
}

tests/ConnectionManagerTimeoutTest.php

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
use ConnectionManager\Extra\ConnectionManagerReject;
44

55
use React\Stream\Stream;
6-
76
use ConnectionManager\Extra\ConnectionManagerDelay;
8-
97
use ConnectionManager\Extra\ConnectionManagerTimeout;
8+
use React\Promise\Promise;
9+
use React\Promise\Timer;
1010

1111
class ConnectionManagerTimeoutTest extends TestCase
1212
{
@@ -53,4 +53,47 @@ public function testTimeoutAbort()
5353
$this->loop->run();
5454
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
5555
}
56+
57+
public function testWillEndConnectionIfConnectionResolvesDespiteTimeout()
58+
{
59+
$stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->getMock();
60+
$stream->expects($this->once())->method('end');
61+
62+
$loop = $this->loop;
63+
$promise = new Promise(function ($resolve) use ($loop, $stream) {
64+
$loop->addTimer(0.002, function () use ($resolve, $stream) {
65+
$resolve($stream);
66+
});
67+
});
68+
69+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
70+
$connector->expects($this->once())->method('create')->with('www.google.com', 80)->willReturn($promise);
71+
72+
$cm = new ConnectionManagerTimeout($connector, $this->loop, 0.001);
73+
74+
$promise = $cm->create('www.google.com', 80);
75+
76+
$this->loop->run();
77+
78+
$this->assertPromiseReject($promise);
79+
}
80+
81+
public function testCancellationOfPromiseWillCancelConnectionAttempt()
82+
{
83+
$promise = new Promise(function () {}, function () {
84+
throw new \RuntimeException();
85+
});
86+
87+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
88+
$connector->expects($this->once())->method('create')->with('www.google.com', 80)->willReturn($promise);
89+
90+
$cm = new ConnectionManagerTimeout($connector, $this->loop, 5.0);
91+
92+
$promise = $cm->create('www.google.com', 80);
93+
$promise->cancel();
94+
95+
$this->loop->run();
96+
97+
$this->assertPromiseReject($promise);
98+
}
5699
}

tests/Multiple/ConnectionManagerConsecutiveTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use ConnectionManager\Extra\Multiple\ConnectionManagerConsecutive;
44
use ConnectionManager\Extra\ConnectionManagerReject;
5+
use React\Promise;
56

67
class ConnectionManagerConsecutiveTest extends TestCase
78
{
@@ -29,4 +30,35 @@ public function testReject()
2930

3031
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
3132
}
33+
34+
public function testWillTryAllIfEachRejects()
35+
{
36+
$rejected = Promise\reject(new \RuntimeException('nope'));
37+
38+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
39+
$connector->expects($this->exactly(2))->method('create')->with('google.com', 80)->willReturn($rejected);
40+
41+
$cm = new ConnectionManagerConsecutive();
42+
$cm->addConnectionManager($connector);
43+
$cm->addConnectionManager($connector);
44+
45+
$promise = $cm->create('google.com', 80);
46+
47+
$this->assertPromiseReject($promise);
48+
}
49+
50+
public function testCancellationWillNotStartAnyFurtherConnections()
51+
{
52+
$pending = new Promise\Promise(function () { }, $this->expectCallableOnce());
53+
54+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
55+
$connector->expects($this->once())->method('create')->with('google.com', 80)->willReturn($pending);
56+
57+
$cm = new ConnectionManagerConsecutive();
58+
$cm->addConnectionManager($connector);
59+
$cm->addConnectionManager($connector);
60+
61+
$promise = $cm->create('google.com', 80);
62+
$promise->cancel();
63+
}
3264
}

tests/Multiple/ConnectionManagerRandomTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use ConnectionManager\Extra\Multiple\ConnectionManagerRandom;
44
use ConnectionManager\Extra\ConnectionManagerReject;
5+
use React\Promise;
56

67
class ConnectionManagerRandomTest extends TestCase
78
{
@@ -29,4 +30,35 @@ public function testReject()
2930

3031
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
3132
}
33+
34+
public function testWillTryAllIfEachRejects()
35+
{
36+
$rejected = Promise\reject(new \RuntimeException('nope'));
37+
38+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
39+
$connector->expects($this->exactly(2))->method('create')->with('google.com', 80)->willReturn($rejected);
40+
41+
$cm = new ConnectionManagerRandom();
42+
$cm->addConnectionManager($connector);
43+
$cm->addConnectionManager($connector);
44+
45+
$promise = $cm->create('google.com', 80);
46+
47+
$this->assertPromiseReject($promise);
48+
}
49+
50+
public function testCancellationWillNotStartAnyFurtherConnections()
51+
{
52+
$pending = new Promise\Promise(function () { }, $this->expectCallableOnce());
53+
54+
$connector = $this->getMock('React\SocketClient\ConnectorInterface');
55+
$connector->expects($this->once())->method('create')->with('google.com', 80)->willReturn($pending);
56+
57+
$cm = new ConnectionManagerRandom();
58+
$cm->addConnectionManager($connector);
59+
$cm->addConnectionManager($connector);
60+
61+
$promise = $cm->create('google.com', 80);
62+
$promise->cancel();
63+
}
3264
}

0 commit comments

Comments
 (0)