Skip to content

Commit a6d23c8

Browse files
committed
Support cancellation of FallbackExecutor
1 parent 1030e75 commit a6d23c8

File tree

2 files changed

+96
-8
lines changed

2 files changed

+96
-8
lines changed

src/Query/FallbackExecutor.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace React\Dns\Query;
44

5+
use React\Promise\Promise;
6+
57
final class FallbackExecutor implements ExecutorInterface
68
{
79
private $executor;
@@ -15,16 +17,33 @@ public function __construct(ExecutorInterface $executor, ExecutorInterface $fall
1517

1618
public function query(Query $query)
1719
{
20+
$cancelled = false;
1821
$fallback = $this->fallback;
19-
return $this->executor->query($query)->then(null, function (\Exception $e1) use ($query, $fallback) {
20-
return $fallback->query($query)->then(null, function (\Exception $e2) use ($e1) {
21-
$append = $e2->getMessage();
22-
if (($pos = strpos($append, ':')) !== false) {
23-
$append = substr($append, $pos + 2);
22+
$promise = $this->executor->query($query);
23+
24+
return new Promise(function ($resolve, $reject) use (&$promise, $fallback, $query, &$cancelled) {
25+
$promise->then($resolve, function (\Exception $e1) use ($fallback, $query, $resolve, $reject, &$cancelled, &$promise) {
26+
// reject if primary resolution rejected due to cancellation
27+
if ($cancelled) {
28+
$reject($e1);
29+
return;
2430
}
2531

26-
throw new \RuntimeException($e1->getMessage() . '. ' . $append);
32+
// start fallback query if primary query rejected
33+
$promise = $fallback->query($query)->then($resolve, function (\Exception $e2) use ($e1, $reject) {
34+
$append = $e2->getMessage();
35+
if (($pos = strpos($append, ':')) !== false) {
36+
$append = substr($append, $pos + 2);
37+
}
38+
39+
// reject with combined error message if both queries fail
40+
$reject(new \RuntimeException($e1->getMessage() . '. ' . $append));
41+
});
2742
});
43+
}, function () use (&$promise, &$cancelled) {
44+
// cancel pending query (primary or fallback)
45+
$cancelled = true;
46+
$promise->cancel();
2847
});
2948
}
3049
}

tests/Query/FallbackExecutorTest.php

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,6 @@ public function testQueryWillRejectWithExceptionMessagesConcatenatedInFullWhenPr
134134

135135
public function testCancelQueryWillReturnRejectedPromiseWithoutCallingSecondaryExecutorWhenPrimaryExecutorIsStillPending()
136136
{
137-
$this->markTestIncomplete();
138-
139137
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
140138

141139
$primary = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
@@ -152,4 +150,75 @@ public function testCancelQueryWillReturnRejectedPromiseWithoutCallingSecondaryE
152150
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
153151
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
154152
}
153+
154+
public function testCancelQueryWillReturnRejectedPromiseWhenPrimaryExecutorRejectsAndSecondaryExecutorIsStillPending()
155+
{
156+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
157+
158+
$primary = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
159+
$primary->expects($this->once())->method('query')->with($query)->willReturn(\React\Promise\reject(new \RuntimeException()));
160+
161+
$secondary = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
162+
$secondary->expects($this->once())->method('query')->with($query)->willReturn(new Promise(function () { }, function () { throw new \RuntimeException(); }));
163+
164+
$executor = new FallbackExecutor($primary, $secondary);
165+
166+
$promise = $executor->query($query);
167+
$promise->cancel();
168+
169+
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
170+
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
171+
}
172+
173+
public function testCancelQueryShouldNotCauseGarbageReferencesWhenCancellingPrimaryExecutor()
174+
{
175+
if (class_exists('React\Promise\When')) {
176+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
177+
}
178+
179+
$primary = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
180+
$primary->expects($this->once())->method('query')->willReturn(new Promise(function () { }, function () { throw new \RuntimeException(); }));
181+
182+
$secondary = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
183+
$secondary->expects($this->never())->method('query');
184+
185+
$executor = new FallbackExecutor($primary, $secondary);
186+
187+
gc_collect_cycles();
188+
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
189+
190+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
191+
192+
$promise = $executor->query($query);
193+
$promise->cancel();
194+
$promise = null;
195+
196+
$this->assertEquals(0, gc_collect_cycles());
197+
}
198+
199+
public function testCancelQueryShouldNotCauseGarbageReferencesWhenCancellingSecondaryExecutor()
200+
{
201+
if (class_exists('React\Promise\When')) {
202+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
203+
}
204+
205+
$primary = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
206+
$primary->expects($this->once())->method('query')->willReturn(\React\Promise\reject(new \RuntimeException()));
207+
208+
$secondary = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
209+
$secondary->expects($this->once())->method('query')->willReturn(new Promise(function () { }, function () { throw new \RuntimeException(); }));
210+
211+
$executor = new FallbackExecutor($primary, $secondary);
212+
213+
gc_collect_cycles();
214+
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
215+
216+
$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
217+
218+
$promise = $executor->query($query);
219+
$promise->cancel();
220+
$promise = null;
221+
222+
$this->assertEquals(0, gc_collect_cycles());
223+
}
155224
}

0 commit comments

Comments
 (0)