diff --git a/README.md b/README.md index f2eeb6b..be1f8b2 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,8 @@ 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); @@ -115,7 +116,7 @@ $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 { @@ -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( @@ -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( @@ -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). diff --git a/composer.json b/composer.json index 8baf0dc..f28bce8 100644 --- a/composer.json +++ b/composer.json @@ -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" } } diff --git a/src/functions.php b/src/functions.php index 09d09eb..8f4e6d1 100644 --- a/src/functions.php +++ b/src/functions.php @@ -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 @@ -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; @@ -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 @@ -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); } @@ -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); diff --git a/tests/FunctionAwaitAllTest.php b/tests/FunctionAwaitAllTest.php index f3db7ba..10e66e8 100644 --- a/tests/FunctionAwaitAllTest.php +++ b/tests/FunctionAwaitAllTest.php @@ -2,6 +2,7 @@ use Clue\React\Block; use React\Promise; +use React\Promise\Timer\TimeoutException; class FunctionAwaitAllTest extends TestCase { @@ -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); + } + } } diff --git a/tests/FunctionAwaitAnyTest.php b/tests/FunctionAwaitAnyTest.php index e40fbe3..3747b34 100644 --- a/tests/FunctionAwaitAnyTest.php +++ b/tests/FunctionAwaitAnyTest.php @@ -3,6 +3,7 @@ use Clue\React\Block; use React\Promise\Deferred; use React\Promise; +use React\Promise\Timer\TimeoutException; class FunctionAwaitAnyTest extends TestCase { @@ -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); + } + } } diff --git a/tests/FunctionAwaitTest.php b/tests/FunctionAwaitTest.php index 6b5e126..75f8fa1 100644 --- a/tests/FunctionAwaitTest.php +++ b/tests/FunctionAwaitTest.php @@ -1,6 +1,8 @@ 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); + } }