Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 107 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,91 @@ If no cancellation handler is passed to the `Promise` constructor, then invoking
its `cancel()` method it is effectively a NO-OP.
This means that it may still be pending and can hence continue consuming resources.

> Note: If you're stuck on legacy versions (PHP 5.3), then this is also a NO-OP,
as the Promise cancellation API is currently only available in
[react/promise v2.1.0](https://github.com/reactphp/promise)
which in turn requires PHP 5.4 or up.
It is assumed that if you're actually still stuck on PHP 5.3, resource cleanup
is likely one of your smaller problems.

For more details on the promise cancellation, please refer to the
[Promise documentation](https://github.com/reactphp/promise#cancellablepromiseinterface).

#### Input cancellation

Irrespective of the timout handling, you can also explicitly `cancel()` the
input `$promise` at any time.
This means that the `timeout()` handling does not affect cancellation of the
input `$promise`, as demonstrated in the following example:

```php
$promise = accessSomeRemoteResource();
$timeout = Timer\timeout($promise, 10.0, $loop);

$promise->cancel();
```

The registered [cancellation handler](#cancellation-handler) is responsible for
handling the `cancel()` call:

* A described above, a common use involves resource cleanup and will then *reject*
the `Promise`.
If the input `$promise` is being rejected, then the timeout will be aborted
and the resulting promise will also be rejected.
* If the input `$promise` is still pending, then the timout will continue
running until the timer expires.
The same happens if the input `$promise` does not register a
[cancellation handler](#cancellation-handler).

#### Output cancellation

Similarily, you can also explicitly `cancel()` the resulting promise like this:

```php
$promise = accessSomeRemoteResource();
$timeout = Timer\timeout($promise, 10.0, $loop);

$timeout->cancel();
```

Note how this looks very similar to the above [input cancellation](#input-cancellation)
example. Accordingly, it also behaves very similar.

Calling `cancel()` on the resulting promise will merely try
to `cancel()` the input `$promise`.
This means that we do not take over responsibility of the outcome and it's
entirely up to the input `$promise` to handle cancellation support.

The registered [cancellation handler](#cancellation-handler) is responsible for
handling the `cancel()` call:

* As described above, a common use involves resource cleanup and will then *reject*
the `Promise`.
If the input `$promise` is being rejected, then the timeout will be aborted
and the resulting promise will also be rejected.
* If the input `$promise` is still pending, then the timout will continue
running until the timer expires.
The same happens if the input `$promise` does not register a
[cancellation handler](#cancellation-handler).

To re-iterate, note that calling `cancel()` on the resulting promise will merely
try to cancel the input `$promise` only.
It is then up to the cancellation handler of the input promise to settle the promise.
If the input promise is still pending when the timeout occurs, then the normal
[timeout cancellation](#timeout-cancellation) handling will trigger, effectively rejecting
the output promise with a [`TimeoutException`](#timeoutexception).

This is done for consistency with the [timeout cancellation](#timeout-cancellation)
handling and also because it is assumed this is often used like this:

```php
$timeout = Timer\timeout(accessSomeRemoteResource(), 10.0, $loop);

$timeout->cancel();
```

As described above, this example works as expected and cleans up any resources
allocated for the input `$promise`.

Note that if the given input `$promise` does not support cancellation, then this
is a NO-OP.
This means that while the resulting promise will still be rejected after the
timeout, the underlying input `$promise` may still be pending and can hence
continue consuming resources.

#### Collections

If you want to wait for multiple promises to resolve, you can use the normal promise primitives like this:
Expand Down Expand Up @@ -176,20 +251,44 @@ Timer\resolve(1.5, $loop)->then(function ($time) {
});
```

#### Resolve cancellation

You can explicitly `cancel()` the resulting timer promise at any time:

```php
$timer = Timer\resolve(2.0, $loop);

$timer->cancel();
```

This will abort the timer and *reject* with a `RuntimeException`.

### reject()

The `reject($time, LoopInterface $loop)` function can be used to create a new Promise
which rejects in `$time` seconds with a `TimeoutException`.

```php
Timer\reject(2.0, $loop)->then(null, function (TimeoutException $e) {
echo '
echo 'Rejected after ' . $e->getTimeout() . ' seconds ' . PHP_EOL;
});
```

This function complements the [`resolve()`](#resolve) function
and can be used as a basic building block for higher-level promise consumers.

#### Reject cancellation

You can explicitly `cancel()` the resulting timer promise at any time:

```php
$timer = Timer\reject(2.0, $loop);

$timer->cancel();
```

This will abort the timer and *reject* with a `RuntimeException`.

### TimeoutException

The `TimeoutException` extends PHP's built-in `RuntimeException`.
Expand All @@ -209,12 +308,6 @@ The recommended way to install this library is [through composer](http://getcomp
}
```

> Note: If you're stuck on legacy versions (PHP 5.3), then the `cancel()` method
is not available,
as the Promise cancellation API is currently only available in
[react/promise v2.1.0](https://github.com/reactphp/promise)
which in turn requires PHP 5.4 or up.

## License

MIT
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"require": {
"php": ">=5.3",
"react/event-loop": "~0.4.0|~0.3.0",
"react/promise": "~2.0|~1.1"
"react/promise": "~2.1|~1.2"
}
}
18 changes: 15 additions & 3 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@

function timeout(PromiseInterface $promise, $time, LoopInterface $loop)
{
// cancelling this promise will only try to cancel the input promise,
// thus leaving responsibility to the input promise.
$canceller = null;
if ($promise instanceof CancellablePromiseInterface) {
$canceller = array($promise, 'cancel');
}

return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) {
$timer = $loop->addTimer($time, function () use ($time, $promise, $reject) {
$reject(new TimeoutException($time, 'Timed out after ' . $time . ' seconds'));
Expand All @@ -25,15 +32,20 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop)
$loop->cancelTimer($timer);
$reject($v);
});
});
}, $canceller);
}

function resolve($time, LoopInterface $loop)
{
return new Promise(function ($resolve) use ($loop, $time) {
$loop->addTimer($time, function () use ($time, $resolve) {
return new Promise(function ($resolve) use ($loop, $time, &$timer) {
// resolve the promise when the timer fires in $time seconds
$timer = $loop->addTimer($time, function () use ($time, $resolve) {
$resolve($time);
});
}, function ($resolveUnused, $reject) use (&$timer, $loop) {
// cancelling this promise will cancel the timer and reject
$loop->cancelTimer($timer);
$reject(new \RuntimeException('Timer cancelled'));
});
}

Expand Down
10 changes: 10 additions & 0 deletions tests/FunctionRejectTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use React\Promise\Timer;
use React\Promise\CancellablePromiseInterface;

class FunctionRejectTest extends TestCase
{
Expand All @@ -19,4 +20,13 @@ public function testPromiseWillBeRejectedOnTimeout()

$this->expectPromiseRejected($promise);
}

public function testCancelingPromiseWillRejectTimer()
{
$promise = Timer\reject(0.01, $this->loop);

$promise->cancel();

$this->expectPromiseRejected($promise);
}
}
32 changes: 32 additions & 0 deletions tests/FunctionResolveTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use React\Promise\Timer;
use React\Promise\CancellablePromiseInterface;

class FunctionResolveTest extends TestCase
{
Expand All @@ -19,4 +20,35 @@ public function testPromiseWillBeResolvedOnTimeout()

$this->expectPromiseResolved($promise);
}

public function testWillStartLoopTimer()
{
$loop = $this->getMock('React\EventLoop\LoopInterface');
$loop->expects($this->once())->method('addTimer')->with($this->equalTo(0.01));

Timer\resolve(0.01, $loop);
}

public function testCancellingPromiseWillCancelLoopTimer()
{
$loop = $this->getMock('React\EventLoop\LoopInterface');

$timer = $this->getMock('React\EventLoop\Timer\TimerInterface');
$loop->expects($this->once())->method('addTimer')->will($this->returnValue($timer));

$promise = Timer\resolve(0.01, $loop);

$loop->expects($this->once())->method('cancelTimer')->with($this->equalTo($timer));

$promise->cancel();
}

public function testCancelingPromiseWillRejectTimer()
{
$promise = Timer\resolve(0.01, $this->loop);

$promise->cancel();

$this->expectPromiseRejected($promise);
}
}
67 changes: 62 additions & 5 deletions tests/FunctionTimeoutTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,73 @@ public function testPendingWillRejectOnTimeout()

public function testPendingCancellableWillBeCancelledOnTimeout()
{
if (!interface_exists('React\Promise\CancellablePromiseInterface', true)) {
$this->markTestSkipped('Your (outdated?) Promise API does not support cancellable promises');
}

$promise = $this->getMock('React\Promise\CancellablePromiseInterface');
$promise->expects($this->once())->method('cancel');


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

$this->loop->run();
}

public function testCancelTimeoutWithoutCancellationhandlerWillNotCancelTimerAndWillNotReject()
{
$promise = new \React\Promise\Promise(function () { });

$loop = $this->getMock('React\EventLoop\LoopInterface');

$timer = $this->getMock('React\EventLoop\Timer\TimerInterface');
$loop->expects($this->once())->method('addTimer')->will($this->returnValue($timer));
$loop->expects($this->never())->method('cancelTimer');

$timeout = Timer\timeout($promise, 0.01, $loop);

$timeout->cancel();

$this->expectPromisePending($timeout);
}

public function testCancelTimeoutWillCancelGivenPromise()
{
$promise = new \React\Promise\Promise(function () { }, $this->expectCallableOnce());

$timeout = Timer\timeout($promise, 0.01, $this->loop);

$timeout->cancel();
}

public function testCancelGivenPromiseWillReject()
{
$promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $reject(); });

$timeout = Timer\timeout($promise, 0.01, $this->loop);

$promise->cancel();

$this->expectPromiseRejected($promise);
$this->expectPromiseRejected($timeout);
}

public function testCancelTimeoutWillRejectIfGivenPromiseWillReject()
{
$promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $reject(); });

$timeout = Timer\timeout($promise, 0.01, $this->loop);

$timeout->cancel();

$this->expectPromiseRejected($promise);
$this->expectPromiseRejected($timeout);
}

public function testCancelTimeoutWillResolveIfGivenPromiseWillResolve()
{
$promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $resolve(); });

$timeout = Timer\timeout($promise, 0.01, $this->loop);

$timeout->cancel();

$this->expectPromiseResolved($promise);
$this->expectPromiseResolved($timeout);
}
}