Skip to content

Commit

Permalink
Merge pull request #17 from clue-labs/timeout
Browse files Browse the repository at this point in the history
Add optional timeout parameter to all await*() functions
  • Loading branch information
clue committed Mar 9, 2016
2 parents d57c4a2 + 99cb7af commit 656c5fd
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 21 deletions.
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,16 @@ If there are no other (async) tasks, this will behave similar to `sleep()`.

#### await()

The `await(PromiseInterface $promise, LoopInterface $loop)` method can be used to block waiting for the given $promise to resolve.
The `await(PromiseInterface $promise, LoopInterface $loop, $timeout = null)`
function can be used to block waiting for the given $promise to resolve.

```php
$result = Block\await($promise, $loop);
```

Once the promise is resolved, this will return whatever the promise resolves to.

If the promises is being rejected, this will fail and throw an `Exception`.
Once the promise is rejected, this will throw whatever the promise rejected with.

```php
try {
Expand All @@ -128,9 +129,16 @@ try {
}
```

If no $timeout is given and the promise stays pending, then this will
potentially wait/block forever until the promise is settled.

If a $timeout is given and the promise is still pending once the timeout
triggers, this will `cancel()` the promise and throw a `TimeoutException`.

#### awaitAny()

The `awaitAny(array $promises, LoopInterface $loop)` method can be used to wait for ANY of the given promises to resolve.
The `awaitAny(array $promises, LoopInterface $loop, $timeout = null)`
function can be used to wait for ANY of the given promises to resolve.

```php
$promises = array(
Expand All @@ -148,9 +156,16 @@ remaining promises and return whatever the first promise resolves to.

If ALL promises fail to resolve, this will fail and throw an `Exception`.

If no $timeout is given and either promise stays pending, then this will
potentially wait/block forever until the last promise is settled.

If a $timeout is given and either promise is still pending once the timeout
triggers, this will `cancel()` all pending promises and throw a `TimeoutException`.

#### awaitAll()

The `awaitAll(array $promises, LoopInterface $loop)` method can be used to wait for ALL of the given promises to resolve.
The `awaitAll(array $promises, LoopInterface $loop, $timeout = null)`
function can be used to wait for ALL of the given promises to resolve.

```php
$promises = array(
Expand All @@ -170,6 +185,12 @@ be used to correlate the return array to the promises passed.
If ANY promise fails to resolve, this will try to `cancel()` all
remaining promises and throw an `Exception`.

If no $timeout is given and either promise stays pending, then this will
potentially wait/block forever until the last promise is settled.

If a $timeout is given and either promise is still pending once the timeout
triggers, this will `cancel()` all pending promises and throw a `TimeoutException`.

## Install

The recommended way to install this library is [through composer](http://getcomposer.org).
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"require": {
"php": ">=5.3",
"react/event-loop": "0.4.*|0.3.*",
"react/promise": "~2.1|~1.2"
"react/promise": "~2.1|~1.2",
"react/promise-timer": "~1.0"
}
}
65 changes: 49 additions & 16 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use UnderflowException;
use Exception;
use React\Promise;
use React\Promise\Timer;
use React\Promise\Timer\TimeoutException;

/**
* wait/sleep for $time seconds
Expand All @@ -17,31 +19,39 @@
*/
function sleep($time, LoopInterface $loop)
{
$wait = true;
$loop->addTimer($time, function () use ($loop, &$wait) {
$loop->stop();
$wait = false;
});

do {
$loop->run();
} while($wait);
await(Timer\resolve($time, $loop), $loop);
}

/**
* block waiting for the given $promise to resolve
*
* Once the promise is resolved, this will return whatever the promise resolves to.
*
* Once the promise is rejected, this will throw whatever the promise rejected with.
*
* If no $timeout is given and the promise stays pending, then this will
* potentially wait/block forever until the promise is settled.
*
* If a $timeout is given and the promise is still pending once the timeout
* triggers, this will cancel() the promise and throw a `TimeoutException`.
*
* @param PromiseInterface $promise
* @param LoopInterface $loop
* @param null|float $timeout (optional) maximum timeout in seconds or null=wait forever
* @return mixed returns whatever the promise resolves to
* @throws Exception when the promise is rejected
* @throws TimeoutException if the $timeout is given and triggers
*/
function await(PromiseInterface $promise, LoopInterface $loop)
function await(PromiseInterface $promise, LoopInterface $loop, $timeout = null)
{
$wait = true;
$resolved = null;
$exception = null;

if ($timeout !== null) {
$promise = Timer\timeout($promise, $timeout, $loop);
}

$promise->then(
function ($c) use (&$resolved, &$wait, $loop) {
$resolved = $c;
Expand Down Expand Up @@ -74,12 +84,20 @@ function ($error) use (&$exception, &$wait, $loop) {
*
* If ALL promises fail to resolve, this will fail and throw an Exception.
*
* If no $timeout is given and either promise stays pending, then this will
* potentially wait/block forever until the last promise is settled.
*
* If a $timeout is given and either promise is still pending once the timeout
* triggers, this will cancel() all pending promises and throw a `TimeoutException`.
*
* @param array $promises
* @param LoopInterface $loop
* @param null|float $timeout (optional) maximum timeout in seconds or null=wait forever
* @return mixed returns whatever the first promise resolves to
* @throws Exception if ALL promises are rejected
* @throws TimeoutException if the $timeout is given and triggers
*/
function awaitAny(array $promises, LoopInterface $loop)
function awaitAny(array $promises, LoopInterface $loop, $timeout = null)
{
try {
// Promise\any() does not cope with an empty input array, so reject this here
Expand All @@ -90,10 +108,17 @@ function awaitAny(array $promises, LoopInterface $loop)
$ret = await(Promise\any($promises)->then(null, function () {
// rejects with an array of rejection reasons => reject with Exception instead
throw new Exception('All promises rejected');
}), $loop);
}), $loop, $timeout);
} catch (TimeoutException $e) {
// the timeout fired
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($promises);

throw $e;
} catch (Exception $e) {
// if the above throws, then ALL promises are already rejected
// (attention: this does not apply once timeout comes into play)
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($promises);

throw new UnderflowException('No promise could resolve', 0, $e);
}
Expand All @@ -115,17 +140,25 @@ function awaitAny(array $promises, LoopInterface $loop)
* If ANY promise fails to resolve, this will try to cancel() all
* remaining promises and throw an Exception.
*
* If no $timeout is given and either promise stays pending, then this will
* potentially wait/block forever until the last promise is settled.
*
* If a $timeout is given and either promise is still pending once the timeout
* triggers, this will cancel() all pending promises and throw a `TimeoutException`.
*
* @param array $promises
* @param LoopInterface $loop
* @param null|float $timeout (optional) maximum timeout in seconds or null=wait forever
* @return array returns an array with whatever each promise resolves to
* @throws Exception when ANY promise is rejected
* @throws TimeoutException if the $timeout is given and triggers
*/
function awaitAll(array $promises, LoopInterface $loop)
function awaitAll(array $promises, LoopInterface $loop, $timeout = null)
{
try {
return await(Promise\all($promises), $loop);
return await(Promise\all($promises), $loop, $timeout);
} catch (Exception $e) {
// ANY of the given promises rejected
// ANY of the given promises rejected or the timeout fired
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($promises);

Expand Down
15 changes: 15 additions & 0 deletions tests/FunctionAwaitAllTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Clue\React\Block;
use React\Promise;
use React\Promise\Timer\TimeoutException;

class FunctionAwaitAllTest extends TestCase
{
Expand Down Expand Up @@ -70,4 +71,18 @@ public function testAwaitAllWithRejectedWillCancelPending()
$this->assertTrue($cancelled);
}
}

public function testAwaitAllPendingWillThrowAndCallCancellerOnTimeout()
{
$cancelled = false;
$promise = new Promise\Promise(function () { }, function () use (&$cancelled) {
$cancelled = true;
});

try {
Block\awaitAll(array($promise), $this->loop, 0.001);
} catch (TimeoutException $expected) {
$this->assertTrue($cancelled);
}
}
}
15 changes: 15 additions & 0 deletions tests/FunctionAwaitAnyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use Clue\React\Block;
use React\Promise\Deferred;
use React\Promise;
use React\Promise\Timer\TimeoutException;

class FunctionAwaitAnyTest extends TestCase
{
Expand Down Expand Up @@ -80,4 +81,18 @@ public function testAwaitAnyWithResolvedWillCancelPending()
$this->assertEquals(2, Block\awaitAny($all, $this->loop));
$this->assertTrue($cancelled);
}

public function testAwaitAnyPendingWillThrowAndCallCancellerOnTimeout()
{
$cancelled = false;
$promise = new Promise\Promise(function () { }, function () use (&$cancelled) {
$cancelled = true;
});

try {
Block\awaitAny(array($promise), $this->loop, 0.001);
} catch (TimeoutException $expected) {
$this->assertTrue($cancelled);
}
}
}
36 changes: 36 additions & 0 deletions tests/FunctionAwaitTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php

use Clue\React\Block;
use React\Promise;
use React\Promise\Timer\TimeoutException;

class FunctionAwaitTest extends TestCase
{
Expand All @@ -26,4 +28,38 @@ public function testAwaitOneInterrupted()

$this->assertEquals(2, Block\await($promise, $this->loop));
}

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

$this->setExpectedException('React\Promise\Timer\TimeoutException');
Block\await($promise, $this->loop, 0.001);
}

public function testAwaitOncePendingWillThrowAndCallCancellerOnTimeout()
{
$cancelled = false;
$promise = new Promise\Promise(function () { }, function () use (&$cancelled) {
$cancelled = true;
});

try {
Block\await($promise, $this->loop, 0.001);
} catch (TimeoutException $expected) {
$this->assertTrue($cancelled);
}
}

public function testAwaitOnceWithTimeoutWillResolvemmediatelyAndCleanUpTimeout()
{
$promise = Promise\resolve(true);

$time = microtime(true);
Block\await($promise, $this->loop, 5.0);
$this->loop->run();
$time = microtime(true) - $time;

$this->assertLessThan(0.1, $time);
}
}

0 comments on commit 656c5fd

Please sign in to comment.