Skip to content

Commit 4ebd159

Browse files
authored
Merge pull request #35 from clue-labs/cancel
Support Promise cancellation
2 parents 5fdf770 + 0cb69f9 commit 4ebd159

7 files changed

Lines changed: 109 additions & 17 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"php": ">=5.3.0",
88
"react/cache": "~0.4.0|~0.3.0",
99
"react/socket": "~0.4.0|~0.3.0",
10-
"react/promise": "~2.0|~1.1"
10+
"react/promise": "~2.1|~1.2"
1111
},
1212
"autoload": {
1313
"psr-4": { "React\\Dns\\": "src" }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace React\Dns\Query;
4+
5+
class CancellationException extends \RuntimeException
6+
{
7+
}

src/Query/Executor.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ public function doQuery($nameserver, $transport, $queryData, $name)
5353
$loop = $this->loop;
5454

5555
$response = new Message();
56-
$deferred = new Deferred();
56+
$deferred = new Deferred(function ($resolve, $reject) use (&$timer, &$conn, $name) {
57+
$reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name)));
58+
59+
$timer->cancel();
60+
$conn->close();
61+
});
5762

5863
$retryWithTcp = function () use ($that, $nameserver, $queryData, $name) {
5964
return $that->doQuery($nameserver, 'tcp', $queryData, $name);

src/Query/RetryExecutor.php

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,28 @@ public function __construct(ExecutorInterface $executor, $retries = 2)
1717

1818
public function query($nameserver, Query $query)
1919
{
20-
$deferred = new Deferred();
21-
22-
$this->tryQuery($nameserver, $query, $this->retries, $deferred);
23-
24-
return $deferred->promise();
20+
return $this->tryQuery($nameserver, $query, $this->retries);
2521
}
2622

27-
public function tryQuery($nameserver, Query $query, $retries, $deferred)
23+
public function tryQuery($nameserver, Query $query, $retries)
2824
{
2925
$that = $this;
30-
$errorback = function ($error) use ($nameserver, $query, $retries, $deferred, $that) {
26+
$errorback = function ($error) use ($nameserver, $query, $retries, $that) {
3127
if (!$error instanceof TimeoutException) {
32-
$deferred->reject($error);
33-
return;
28+
throw $error;
3429
}
3530
if (0 >= $retries) {
36-
$error = new \RuntimeException(
31+
throw new \RuntimeException(
3732
sprintf("DNS query for %s failed: too many retries", $query->name),
3833
0,
3934
$error
4035
);
41-
$deferred->reject($error);
42-
return;
4336
}
44-
$that->tryQuery($nameserver, $query, $retries-1, $deferred);
37+
return $that->tryQuery($nameserver, $query, $retries-1);
4538
};
4639

47-
$this->executor
40+
return $this->executor
4841
->query($nameserver, $query)
49-
->then(array($deferred, 'resolve'), $errorback);
42+
->then(null, $errorback);
5043
}
5144
}

tests/FunctionalResolverTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,17 @@ public function testResolveInvalidRejects()
3232

3333
$this->loop->run();
3434
}
35+
36+
public function testResolveCancelledRejectsImmediately()
37+
{
38+
$promise = $this->resolver->resolve('google.com');
39+
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
40+
$promise->cancel();
41+
42+
$time = microtime(true);
43+
$this->loop->run();
44+
$time = microtime(true) - $time;
45+
46+
$this->assertLessThan(0.1, $time);
47+
}
3548
}

tests/Query/ExecutorTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,43 @@ public function resolveShouldCreateTcpRequestIfRequestIsLargerThan512Bytes()
6161
$this->executor->query('8.8.8.8:53', $query, function () {}, function () {});
6262
}
6363

64+
/** @test */
65+
public function resolveShouldCloseConnectionWhenCancelled()
66+
{
67+
$conn = $this->createConnectionMock();
68+
$conn->expects($this->once())->method('close');
69+
70+
$timer = $this->getMock('React\EventLoop\Timer\TimerInterface');
71+
$this->loop
72+
->expects($this->any())
73+
->method('addTimer')
74+
->will($this->returnValue($timer));
75+
76+
$this->executor = $this->createExecutorMock();
77+
$this->executor
78+
->expects($this->once())
79+
->method('createConnection')
80+
->with('8.8.8.8:53', 'udp')
81+
->will($this->returnValue($conn));
82+
83+
$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
84+
$promise = $this->executor->query('8.8.8.8:53', $query);
85+
86+
$promise->cancel();
87+
88+
$errorback = $this->createCallableMock();
89+
$errorback
90+
->expects($this->once())
91+
->method('__invoke')
92+
->with($this->logicalAnd(
93+
$this->isInstanceOf('React\Dns\Query\CancellationException'),
94+
$this->attribute($this->equalTo('DNS query for igor.io has been cancelled'), 'message')
95+
)
96+
);
97+
98+
$promise->then($this->expectCallableNever(), $errorback);
99+
}
100+
64101
/** @test */
65102
public function resolveShouldRetryWithTcpIfResponseIsTruncated()
66103
{

tests/Query/RetryExecutorTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use React\Dns\Query\TimeoutException;
1010
use React\Dns\Model\Record;
1111
use React\Promise;
12+
use React\Promise\Deferred;
13+
use React\Dns\Query\CancellationException;
1214

1315
class RetryExecutorTest extends TestCase
1416
{
@@ -125,6 +127,41 @@ public function queryShouldForwardNonTimeoutErrors()
125127
$retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback);
126128
}
127129

130+
/**
131+
* @covers React\Dns\Query\RetryExecutor
132+
* @test
133+
*/
134+
public function queryShouldCancelQueryOnCancel()
135+
{
136+
$cancelled = 0;
137+
138+
$executor = $this->createExecutorMock();
139+
$executor
140+
->expects($this->once())
141+
->method('query')
142+
->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
143+
->will($this->returnCallback(function ($domain, $query) use (&$cancelled) {
144+
$deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
145+
++$cancelled;
146+
$reject(new CancellationException('Cancelled'));
147+
});
148+
149+
return $deferred->promise();
150+
})
151+
);
152+
153+
$retryExecutor = new RetryExecutor($executor, 2);
154+
155+
$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
156+
$promise = $retryExecutor->query('8.8.8.8', $query);
157+
158+
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
159+
160+
$this->assertEquals(0, $cancelled);
161+
$promise->cancel();
162+
$this->assertEquals(1, $cancelled);
163+
}
164+
128165
protected function expectCallableOnce()
129166
{
130167
$mock = $this->createCallableMock();

0 commit comments

Comments
 (0)