Skip to content

Commit 3a7d5b7

Browse files
authored
Merge pull request #89 from clue-labs/cancellation
Improve promise cancellation and close underlying socket connection
2 parents 0aaacb8 + 45e8ef0 commit 3a7d5b7

File tree

2 files changed

+83
-4
lines changed

2 files changed

+83
-4
lines changed

src/Connector.php

+16-4
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,21 @@ public function __invoke($url, array $subProtocols = [], array $headers = []) {
4949

5050
$uriString = $scheme . '://' . $uri->getHost() . ':' . $port;
5151

52-
return $connector->connect($uriString)->then(function(ConnectionInterface $conn) use ($request, $subProtocols) {
53-
$futureWsConn = new Deferred;
52+
$connecting = $connector->connect($uriString);
5453

54+
$futureWsConn = new Deferred(function ($_, $reject) use ($url, $connecting) {
55+
$reject(new \RuntimeException(
56+
'Connection to ' . $url . ' cancelled during handshake'
57+
));
58+
59+
// either close active connection or cancel pending connection attempt
60+
$connecting->then(function (ConnectionInterface $connection) {
61+
$connection->close();
62+
});
63+
$connecting->cancel();
64+
});
65+
66+
$connecting->then(function(ConnectionInterface $conn) use ($request, $subProtocols, $futureWsConn) {
5567
$earlyClose = function() use ($futureWsConn) {
5668
$futureWsConn->reject(new \RuntimeException('Connection closed before handshake'));
5769
};
@@ -98,9 +110,9 @@ public function __invoke($url, array $subProtocols = [], array $headers = []) {
98110

99111
$stream->on('data', $headerParser);
100112
$stream->write(gPsr\str($request));
113+
}, array($futureWsConn, 'reject'));
101114

102-
return $futureWsConn->promise();
103-
});
115+
return $futureWsConn->promise();
104116
}
105117

106118
/**

tests/unit/ConnectorTest.php

+67
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use Ratchet\Client\Connector;
55
use React\EventLoop\Factory;
66
use React\Promise\RejectedPromise;
7+
use React\Promise\Promise;
78

89
class ConnectorTest extends TestCase
910
{
@@ -36,4 +37,70 @@ public function testSecureConnectionUsesTlsScheme($uri, $expectedConnectorUri) {
3637

3738
$pawlConnector($uri);
3839
}
40+
41+
public function testConnectorRejectsWhenUnderlyingSocketConnectorRejects()
42+
{
43+
$exception = new RuntimeException('Connection failed');
44+
45+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
46+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
47+
$connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject($exception));
48+
49+
$pawlConnector = new Connector($loop, $connector);
50+
51+
$promise = $pawlConnector('ws://localhost');
52+
53+
$actual = null;
54+
$promise->then(null, function ($reason) use (&$actual) {
55+
$actual = $reason;
56+
});
57+
$this->assertSame($exception, $actual);
58+
}
59+
60+
public function testCancelConnectorShouldCancelUnderlyingSocketConnectorWhenSocketConnectionIsPending()
61+
{
62+
$promise = new Promise(function () { }, function () use (&$cancelled) {
63+
++$cancelled;
64+
});
65+
66+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
67+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
68+
$connector->expects($this->once())->method('connect')->willReturn($promise);
69+
70+
$pawlConnector = new Connector($loop, $connector);
71+
72+
$promise = $pawlConnector('ws://localhost');
73+
74+
$this->assertNull($cancelled);
75+
$promise->cancel();
76+
$this->assertEquals(1, $cancelled);
77+
78+
$message = null;
79+
$promise->then(null, function ($reason) use (&$message) {
80+
$message = $reason->getMessage();
81+
});
82+
$this->assertEquals('Connection to ws://localhost cancelled during handshake', $message);
83+
}
84+
85+
public function testCancelConnectorShouldCloseUnderlyingSocketConnectionWhenHandshakeIsPending()
86+
{
87+
$connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
88+
$connection->expects($this->once())->method('close');
89+
90+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
91+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
92+
$connector->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
93+
94+
$pawlConnector = new Connector($loop, $connector);
95+
96+
$promise = $pawlConnector('ws://localhost');
97+
98+
$promise->cancel();
99+
100+
$message = null;
101+
$promise->then(null, function ($reason) use (&$message) {
102+
$message = $reason->getMessage();
103+
});
104+
$this->assertEquals('Connection to ws://localhost cancelled during handshake', $message);
105+
}
39106
}

0 commit comments

Comments
 (0)