Skip to content

Commit 43e4e85

Browse files
authoredOct 12, 2018
Merge pull request #84 from clue-labs/factory-cancellation
Support cancellation of pending connection attempts
2 parents ed7fe8b + e38c8c1 commit 43e4e85

File tree

4 files changed

+113
-14
lines changed

4 files changed

+113
-14
lines changed
 

‎README.md

+13
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ will resolve with a [`ConnectionInterface`](#connectioninterface)
110110
instance on success or will reject with an `Exception` if the URL is
111111
invalid or the connection or authentication fails.
112112

113+
The returned Promise is implemented in such a way that it can be
114+
cancelled when it is still pending. Cancelling a pending promise will
115+
reject its value with an Exception and will cancel the underlying TCP/IP
116+
connection attempt and/or MySQL authentication.
117+
118+
```php
119+
$promise = $factory->createConnection($url);
120+
121+
$loop->addTimer(3.0, function () use ($promise) {
122+
$promise->cancel();
123+
});
124+
```
125+
113126
The `$url` parameter must contain the database host, optional
114127
authentication, port and database to connect to:
115128

‎composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"evenement/evenement": "^3.0 || ^2.1 || ^1.1",
99
"react/event-loop": "^1.0 || ^0.5 || ^0.4",
1010
"react/promise": "^2.7",
11-
"react/socket": "^1.0 || ^0.8"
11+
"react/socket": "^1.1"
1212
},
1313
"require-dev": {
1414
"clue/block-react": "^1.2",

‎src/Factory.php

+40-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use React\MySQL\Io\Connection;
88
use React\MySQL\Io\Executor;
99
use React\MySQL\Io\Parser;
10-
use React\Promise\Promise;
10+
use React\Promise\Deferred;
1111
use React\Promise\PromiseInterface;
1212
use React\Socket\Connector;
1313
use React\Socket\ConnectorInterface;
@@ -81,6 +81,19 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector =
8181
* instance on success or will reject with an `Exception` if the URL is
8282
* invalid or the connection or authentication fails.
8383
*
84+
* The returned Promise is implemented in such a way that it can be
85+
* cancelled when it is still pending. Cancelling a pending promise will
86+
* reject its value with an Exception and will cancel the underlying TCP/IP
87+
* connection attempt and/or MySQL authentication.
88+
*
89+
* ```php
90+
* $promise = $factory->createConnection($url);
91+
*
92+
* $loop->addTimer(3.0, function () use ($promise) {
93+
* $promise->cancel();
94+
* });
95+
* ```
96+
*
8497
* The `$url` parameter must contain the database host, optional
8598
* authentication, port and database to connect to:
8699
*
@@ -113,8 +126,22 @@ public function createConnection($uri)
113126
return \React\Promise\reject(new \InvalidArgumentException('Invalid connect uri given'));
114127
}
115128

116-
$uri = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 3306);
117-
return $this->connector->connect($uri)->then(function (ConnectionInterface $stream) use ($parts) {
129+
$connecting = $this->connector->connect(
130+
$parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 3306)
131+
);
132+
133+
$deferred = new Deferred(function ($_, $reject) use ($connecting) {
134+
// connection cancelled, start with rejecting attempt, then clean up
135+
$reject(new \RuntimeException('Connection to database server cancelled'));
136+
137+
// either close successful connection or cancel pending connection attempt
138+
$connecting->then(function (ConnectionInterface $connection) {
139+
$connection->close();
140+
});
141+
$connecting->cancel();
142+
});
143+
144+
$connecting->then(function (ConnectionInterface $stream) use ($parts, $deferred) {
118145
$executor = new Executor();
119146
$parser = new Parser($stream, $executor);
120147

@@ -126,17 +153,17 @@ public function createConnection($uri)
126153
));
127154
$parser->start();
128155

129-
return new Promise(function ($resolve, $reject) use ($command, $connection, $stream) {
130-
$command->on('success', function () use ($resolve, $connection) {
131-
$resolve($connection);
132-
});
133-
$command->on('error', function ($error) use ($reject, $stream) {
134-
$reject($error);
135-
$stream->close();
136-
});
156+
$command->on('success', function () use ($deferred, $connection) {
157+
$deferred->resolve($connection);
158+
});
159+
$command->on('error', function ($error) use ($deferred, $stream) {
160+
$deferred->reject($error);
161+
$stream->close();
137162
});
138-
}, function ($error) {
139-
throw new \RuntimeException('Unable to connect to database server', 0, $error);
163+
}, function ($error) use ($deferred) {
164+
$deferred->reject(new \RuntimeException('Unable to connect to database server', 0, $error));
140165
});
166+
167+
return $deferred->promise();
141168
}
142169
}

‎tests/FactoryTest.php

+59
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use React\MySQL\ConnectionInterface;
66
use React\MySQL\Factory;
77
use React\Socket\Server;
8+
use React\Promise\Promise;
89

910
class FactoryTest extends BaseTestCase
1011
{
@@ -230,4 +231,62 @@ public function testConnectWithValidAuthCanCloseAndAbortPing()
230231

231232
$loop->run();
232233
}
234+
235+
public function testCancelConnectWillCancelPendingConnection()
236+
{
237+
$pending = new Promise(function () { }, $this->expectCallableOnce());
238+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
239+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
240+
$connector->expects($this->once())->method('connect')->willReturn($pending);
241+
242+
$factory = new Factory($loop, $connector);
243+
$promise = $factory->createConnection('127.0.0.1');
244+
245+
$promise->cancel();
246+
247+
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
248+
$promise->then(null, $this->expectCallableOnceWith($this->callback(function ($e) {
249+
return ($e->getMessage() === 'Connection to database server cancelled');
250+
})));
251+
}
252+
253+
public function testCancelConnectWillCancelPendingConnectionWithRuntimeException()
254+
{
255+
$pending = new Promise(function () { }, function () {
256+
throw new \UnexpectedValueException('ignored');
257+
});
258+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
259+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
260+
$connector->expects($this->once())->method('connect')->willReturn($pending);
261+
262+
$factory = new Factory($loop, $connector);
263+
$promise = $factory->createConnection('127.0.0.1');
264+
265+
$promise->cancel();
266+
267+
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
268+
$promise->then(null, $this->expectCallableOnceWith($this->callback(function ($e) {
269+
return ($e->getMessage() === 'Connection to database server cancelled');
270+
})));
271+
}
272+
273+
public function testCancelConnectDuringAuthenticationWillCloseConnection()
274+
{
275+
$connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
276+
$connection->expects($this->once())->method('close');
277+
278+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
279+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
280+
$connector->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
281+
282+
$factory = new Factory($loop, $connector);
283+
$promise = $factory->createConnection('127.0.0.1');
284+
285+
$promise->cancel();
286+
287+
$promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
288+
$promise->then(null, $this->expectCallableOnceWith($this->callback(function ($e) {
289+
return ($e->getMessage() === 'Connection to database server cancelled');
290+
})));
291+
}
233292
}

0 commit comments

Comments
 (0)
Please sign in to comment.