Skip to content

Commit 656c5fd

Browse files
committed
Merge pull request #17 from clue-labs/timeout
Add optional timeout parameter to all await*() functions
2 parents d57c4a2 + 99cb7af commit 656c5fd

File tree

6 files changed

+142
-21
lines changed

6 files changed

+142
-21
lines changed

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,16 @@ If there are no other (async) tasks, this will behave similar to `sleep()`.
107107

108108
#### await()
109109

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

112113
```php
113114
$result = Block\await($promise, $loop);
114115
```
115116

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

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

120121
```php
121122
try {
@@ -128,9 +129,16 @@ try {
128129
}
129130
```
130131

132+
If no $timeout is given and the promise stays pending, then this will
133+
potentially wait/block forever until the promise is settled.
134+
135+
If a $timeout is given and the promise is still pending once the timeout
136+
triggers, this will `cancel()` the promise and throw a `TimeoutException`.
137+
131138
#### awaitAny()
132139

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

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

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

159+
If no $timeout is given and either promise stays pending, then this will
160+
potentially wait/block forever until the last promise is settled.
161+
162+
If a $timeout is given and either promise is still pending once the timeout
163+
triggers, this will `cancel()` all pending promises and throw a `TimeoutException`.
164+
151165
#### awaitAll()
152166

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

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

188+
If no $timeout is given and either promise stays pending, then this will
189+
potentially wait/block forever until the last promise is settled.
190+
191+
If a $timeout is given and either promise is still pending once the timeout
192+
triggers, this will `cancel()` all pending promises and throw a `TimeoutException`.
193+
173194
## Install
174195

175196
The recommended way to install this library is [through composer](http://getcomposer.org).

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"require": {
1717
"php": ">=5.3",
1818
"react/event-loop": "0.4.*|0.3.*",
19-
"react/promise": "~2.1|~1.2"
19+
"react/promise": "~2.1|~1.2",
20+
"react/promise-timer": "~1.0"
2021
}
2122
}

src/functions.php

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use UnderflowException;
99
use Exception;
1010
use React\Promise;
11+
use React\Promise\Timer;
12+
use React\Promise\Timer\TimeoutException;
1113

1214
/**
1315
* wait/sleep for $time seconds
@@ -17,31 +19,39 @@
1719
*/
1820
function sleep($time, LoopInterface $loop)
1921
{
20-
$wait = true;
21-
$loop->addTimer($time, function () use ($loop, &$wait) {
22-
$loop->stop();
23-
$wait = false;
24-
});
25-
26-
do {
27-
$loop->run();
28-
} while($wait);
22+
await(Timer\resolve($time, $loop), $loop);
2923
}
3024

3125
/**
3226
* block waiting for the given $promise to resolve
3327
*
28+
* Once the promise is resolved, this will return whatever the promise resolves to.
29+
*
30+
* Once the promise is rejected, this will throw whatever the promise rejected with.
31+
*
32+
* If no $timeout is given and the promise stays pending, then this will
33+
* potentially wait/block forever until the promise is settled.
34+
*
35+
* If a $timeout is given and the promise is still pending once the timeout
36+
* triggers, this will cancel() the promise and throw a `TimeoutException`.
37+
*
3438
* @param PromiseInterface $promise
3539
* @param LoopInterface $loop
40+
* @param null|float $timeout (optional) maximum timeout in seconds or null=wait forever
3641
* @return mixed returns whatever the promise resolves to
3742
* @throws Exception when the promise is rejected
43+
* @throws TimeoutException if the $timeout is given and triggers
3844
*/
39-
function await(PromiseInterface $promise, LoopInterface $loop)
45+
function await(PromiseInterface $promise, LoopInterface $loop, $timeout = null)
4046
{
4147
$wait = true;
4248
$resolved = null;
4349
$exception = null;
4450

51+
if ($timeout !== null) {
52+
$promise = Timer\timeout($promise, $timeout, $loop);
53+
}
54+
4555
$promise->then(
4656
function ($c) use (&$resolved, &$wait, $loop) {
4757
$resolved = $c;
@@ -74,12 +84,20 @@ function ($error) use (&$exception, &$wait, $loop) {
7484
*
7585
* If ALL promises fail to resolve, this will fail and throw an Exception.
7686
*
87+
* If no $timeout is given and either promise stays pending, then this will
88+
* potentially wait/block forever until the last promise is settled.
89+
*
90+
* If a $timeout is given and either promise is still pending once the timeout
91+
* triggers, this will cancel() all pending promises and throw a `TimeoutException`.
92+
*
7793
* @param array $promises
7894
* @param LoopInterface $loop
95+
* @param null|float $timeout (optional) maximum timeout in seconds or null=wait forever
7996
* @return mixed returns whatever the first promise resolves to
8097
* @throws Exception if ALL promises are rejected
98+
* @throws TimeoutException if the $timeout is given and triggers
8199
*/
82-
function awaitAny(array $promises, LoopInterface $loop)
100+
function awaitAny(array $promises, LoopInterface $loop, $timeout = null)
83101
{
84102
try {
85103
// Promise\any() does not cope with an empty input array, so reject this here
@@ -90,10 +108,17 @@ function awaitAny(array $promises, LoopInterface $loop)
90108
$ret = await(Promise\any($promises)->then(null, function () {
91109
// rejects with an array of rejection reasons => reject with Exception instead
92110
throw new Exception('All promises rejected');
93-
}), $loop);
111+
}), $loop, $timeout);
112+
} catch (TimeoutException $e) {
113+
// the timeout fired
114+
// => try to cancel all promises (rejected ones will be ignored anyway)
115+
_cancelAllPromises($promises);
116+
117+
throw $e;
94118
} catch (Exception $e) {
95119
// if the above throws, then ALL promises are already rejected
96-
// (attention: this does not apply once timeout comes into play)
120+
// => try to cancel all promises (rejected ones will be ignored anyway)
121+
_cancelAllPromises($promises);
97122

98123
throw new UnderflowException('No promise could resolve', 0, $e);
99124
}
@@ -115,17 +140,25 @@ function awaitAny(array $promises, LoopInterface $loop)
115140
* If ANY promise fails to resolve, this will try to cancel() all
116141
* remaining promises and throw an Exception.
117142
*
143+
* If no $timeout is given and either promise stays pending, then this will
144+
* potentially wait/block forever until the last promise is settled.
145+
*
146+
* If a $timeout is given and either promise is still pending once the timeout
147+
* triggers, this will cancel() all pending promises and throw a `TimeoutException`.
148+
*
118149
* @param array $promises
119150
* @param LoopInterface $loop
151+
* @param null|float $timeout (optional) maximum timeout in seconds or null=wait forever
120152
* @return array returns an array with whatever each promise resolves to
121153
* @throws Exception when ANY promise is rejected
154+
* @throws TimeoutException if the $timeout is given and triggers
122155
*/
123-
function awaitAll(array $promises, LoopInterface $loop)
156+
function awaitAll(array $promises, LoopInterface $loop, $timeout = null)
124157
{
125158
try {
126-
return await(Promise\all($promises), $loop);
159+
return await(Promise\all($promises), $loop, $timeout);
127160
} catch (Exception $e) {
128-
// ANY of the given promises rejected
161+
// ANY of the given promises rejected or the timeout fired
129162
// => try to cancel all promises (rejected ones will be ignored anyway)
130163
_cancelAllPromises($promises);
131164

tests/FunctionAwaitAllTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use Clue\React\Block;
44
use React\Promise;
5+
use React\Promise\Timer\TimeoutException;
56

67
class FunctionAwaitAllTest extends TestCase
78
{
@@ -70,4 +71,18 @@ public function testAwaitAllWithRejectedWillCancelPending()
7071
$this->assertTrue($cancelled);
7172
}
7273
}
74+
75+
public function testAwaitAllPendingWillThrowAndCallCancellerOnTimeout()
76+
{
77+
$cancelled = false;
78+
$promise = new Promise\Promise(function () { }, function () use (&$cancelled) {
79+
$cancelled = true;
80+
});
81+
82+
try {
83+
Block\awaitAll(array($promise), $this->loop, 0.001);
84+
} catch (TimeoutException $expected) {
85+
$this->assertTrue($cancelled);
86+
}
87+
}
7388
}

tests/FunctionAwaitAnyTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use Clue\React\Block;
44
use React\Promise\Deferred;
55
use React\Promise;
6+
use React\Promise\Timer\TimeoutException;
67

78
class FunctionAwaitAnyTest extends TestCase
89
{
@@ -80,4 +81,18 @@ public function testAwaitAnyWithResolvedWillCancelPending()
8081
$this->assertEquals(2, Block\awaitAny($all, $this->loop));
8182
$this->assertTrue($cancelled);
8283
}
84+
85+
public function testAwaitAnyPendingWillThrowAndCallCancellerOnTimeout()
86+
{
87+
$cancelled = false;
88+
$promise = new Promise\Promise(function () { }, function () use (&$cancelled) {
89+
$cancelled = true;
90+
});
91+
92+
try {
93+
Block\awaitAny(array($promise), $this->loop, 0.001);
94+
} catch (TimeoutException $expected) {
95+
$this->assertTrue($cancelled);
96+
}
97+
}
8398
}

tests/FunctionAwaitTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<?php
22

33
use Clue\React\Block;
4+
use React\Promise;
5+
use React\Promise\Timer\TimeoutException;
46

57
class FunctionAwaitTest extends TestCase
68
{
@@ -26,4 +28,38 @@ public function testAwaitOneInterrupted()
2628

2729
$this->assertEquals(2, Block\await($promise, $this->loop));
2830
}
31+
32+
public function testAwaitOncePendingWillThrowOnTimeout()
33+
{
34+
$promise = new Promise\Promise(function () { });
35+
36+
$this->setExpectedException('React\Promise\Timer\TimeoutException');
37+
Block\await($promise, $this->loop, 0.001);
38+
}
39+
40+
public function testAwaitOncePendingWillThrowAndCallCancellerOnTimeout()
41+
{
42+
$cancelled = false;
43+
$promise = new Promise\Promise(function () { }, function () use (&$cancelled) {
44+
$cancelled = true;
45+
});
46+
47+
try {
48+
Block\await($promise, $this->loop, 0.001);
49+
} catch (TimeoutException $expected) {
50+
$this->assertTrue($cancelled);
51+
}
52+
}
53+
54+
public function testAwaitOnceWithTimeoutWillResolvemmediatelyAndCleanUpTimeout()
55+
{
56+
$promise = Promise\resolve(true);
57+
58+
$time = microtime(true);
59+
Block\await($promise, $this->loop, 5.0);
60+
$this->loop->run();
61+
$time = microtime(true) - $time;
62+
63+
$this->assertLessThan(0.1, $time);
64+
}
2965
}

0 commit comments

Comments
 (0)