Skip to content

Commit 8c423d5

Browse files
committed
Cancelling timeout() promise now cancels input promise
1 parent d182bed commit 8c423d5

3 files changed

Lines changed: 135 additions & 2 deletions

File tree

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,67 @@ is not available, as the Promise cancellation API is currently only available in
173173
[react/promise v2.1.0](https://github.com/reactphp/promise)
174174
which in turn requires PHP 5.4 or up.
175175

176+
#### Output cancellation
177+
178+
Similarily, you can also explicitly `cancel()` the resulting promise like this:
179+
180+
```php
181+
$promise = accessSomeRemoteResource();
182+
$timeout = Timer\timeout($promise, 10.0, $loop);
183+
184+
$timeout->cancel();
185+
```
186+
187+
Note how this looks very similar to the above [input cancellation](#input-cancellation)
188+
example. Accordingly, it also behaves very similar.
189+
190+
Calling `cancel()` on the resulting promise will merely try
191+
to `cancel()` the input `$promise`.
192+
This means that we do not take over responsibility of the outcome and it's
193+
entirely up to the input `$promise` to handle cancellation support.
194+
195+
The registered [cancellation handler](#cancellation-handler) is responsible for
196+
handling the `cancel()` call:
197+
198+
* As described above, a common use involves resource cleanup and will then *reject*
199+
the `Promise`.
200+
If the input `$promise` is being rejected, then the timeout will be aborted
201+
and the resulting promise will also be rejected.
202+
* If the input `$promise` is still pending, then the timout will continue
203+
running until the timer expires.
204+
The same happens if the input `$promise` does not register a
205+
[cancellation handler](#cancellation-handler).
206+
207+
To re-iterate, note that calling `cancel()` on the resulting promise will merely
208+
try to cancel the input `$promise` only.
209+
It is then up to the cancellation handler of the input promise to settle the promise.
210+
If the input promise is still pending when the timeout occurs, then the normal
211+
[timeout cancellation](#timeout-cancellation) handling will trigger, effectively rejecting
212+
the output promise with a [`TimeoutException`](#timeoutexception).
213+
214+
This is done for consistency with the [timeout cancellation](#timeout-cancellation)
215+
handling and also because it is assumed this is often used like this:
216+
217+
```php
218+
$timeout = Timer\timeout(accessSomeRemoteResource(), 10.0, $loop);
219+
220+
$timeout->cancel();
221+
```
222+
223+
As described above, this example works as expected and cleans up any resources
224+
allocated for the input `$promise`.
225+
226+
Note that if the given input `$promise` does not support cancellation, then this
227+
is a NO-OP.
228+
This means that while the resulting promise will still be rejected after the
229+
timeout, the underlying input `$promise` may still be pending and can hence
230+
continue consuming resources.
231+
232+
> Note: If you're stuck on legacy versions (PHP 5.3), then the `cancel()` method
233+
is not available, as the Promise cancellation API is currently only available in
234+
[react/promise v2.1.0](https://github.com/reactphp/promise)
235+
which in turn requires PHP 5.4 or up.
236+
176237
#### Collections
177238

178239
If you want to wait for multiple promises to resolve, you can use the normal promise primitives like this:

src/functions.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99

1010
function timeout(PromiseInterface $promise, $time, LoopInterface $loop)
1111
{
12+
// cancelling this promise will only try to cancel the input promise,
13+
// thus leaving responsibility to the input promise.
14+
$canceller = null;
15+
if ($promise instanceof CancellablePromiseInterface) {
16+
$canceller = array($promise, 'cancel');
17+
}
18+
1219
return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) {
1320
$timer = $loop->addTimer($time, function () use ($time, $promise, $reject) {
1421
$reject(new TimeoutException($time, 'Timed out after ' . $time . ' seconds'));
@@ -25,7 +32,7 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop)
2532
$loop->cancelTimer($timer);
2633
$reject($v);
2734
});
28-
});
35+
}, $canceller);
2936
}
3037

3138
function resolve($time, LoopInterface $loop)

tests/FunctionTimeoutTest.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,45 @@ public function testPendingCancellableWillBeCancelledOnTimeout()
6969
$promise = $this->getMock('React\Promise\CancellablePromiseInterface');
7070
$promise->expects($this->once())->method('cancel');
7171

72-
7372
Timer\timeout($promise, 0.01, $this->loop);
7473

7574
$this->loop->run();
7675
}
7776

77+
public function testCancelTimeoutWithoutCancellationhandlerWillNotCancelTimerAndWillNotReject()
78+
{
79+
if (!interface_exists('React\Promise\CancellablePromiseInterface', true)) {
80+
$this->markTestSkipped('Your (outdated?) Promise API does not support cancellable promises');
81+
}
82+
83+
$promise = new \React\Promise\Promise(function () { });
84+
85+
$loop = $this->getMock('React\EventLoop\LoopInterface');
86+
87+
$timer = $this->getMock('React\EventLoop\Timer\TimerInterface');
88+
$loop->expects($this->once())->method('addTimer')->will($this->returnValue($timer));
89+
$loop->expects($this->never())->method('cancelTimer');
90+
91+
$timeout = Timer\timeout($promise, 0.01, $loop);
92+
93+
$timeout->cancel();
94+
95+
$this->expectPromisePending($timeout);
96+
}
97+
98+
public function testCancelTimeoutWillCancelGivenPromise()
99+
{
100+
if (!interface_exists('React\Promise\CancellablePromiseInterface', true)) {
101+
$this->markTestSkipped('Your (outdated?) Promise API does not support cancellable promises');
102+
}
103+
104+
$promise = new \React\Promise\Promise(function () { }, $this->expectCallableOnce());
105+
106+
$timeout = Timer\timeout($promise, 0.01, $this->loop);
107+
108+
$timeout->cancel();
109+
}
110+
78111
public function testCancelGivenPromiseWillReject()
79112
{
80113
if (!interface_exists('React\Promise\CancellablePromiseInterface', true)) {
@@ -90,4 +123,36 @@ public function testCancelGivenPromiseWillReject()
90123
$this->expectPromiseRejected($promise);
91124
$this->expectPromiseRejected($timeout);
92125
}
126+
127+
public function testCancelTimeoutWillRejectIfGivenPromiseWillReject()
128+
{
129+
if (!interface_exists('React\Promise\CancellablePromiseInterface', true)) {
130+
$this->markTestSkipped('Your (outdated?) Promise API does not support cancellable promises');
131+
}
132+
133+
$promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $reject(); });
134+
135+
$timeout = Timer\timeout($promise, 0.01, $this->loop);
136+
137+
$timeout->cancel();
138+
139+
$this->expectPromiseRejected($promise);
140+
$this->expectPromiseRejected($timeout);
141+
}
142+
143+
public function testCancelTimeoutWillResolveIfGivenPromiseWillResolve()
144+
{
145+
if (!interface_exists('React\Promise\CancellablePromiseInterface', true)) {
146+
$this->markTestSkipped('Your (outdated?) Promise API does not support cancellable promises');
147+
}
148+
149+
$promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $resolve(); });
150+
151+
$timeout = Timer\timeout($promise, 0.01, $this->loop);
152+
153+
$timeout->cancel();
154+
155+
$this->expectPromiseResolved($promise);
156+
$this->expectPromiseResolved($timeout);
157+
}
93158
}

0 commit comments

Comments
 (0)