| 1 |
liveuser |
1 |
# PromiseTimer
|
|
|
2 |
|
|
|
3 |
[](https://travis-ci.org/reactphp/promise-timer)
|
|
|
4 |
|
|
|
5 |
A trivial implementation of timeouts for `Promise`s, built on top of [ReactPHP](https://reactphp.org/).
|
|
|
6 |
|
|
|
7 |
**Table of contents**
|
|
|
8 |
|
|
|
9 |
* [Usage](#usage)
|
|
|
10 |
* [timeout()](#timeout)
|
|
|
11 |
* [Timeout cancellation](#timeout-cancellation)
|
|
|
12 |
* [Cancellation handler](#cancellation-handler)
|
|
|
13 |
* [Input cancellation](#input-cancellation)
|
|
|
14 |
* [Output cancellation](#output-cancellation)
|
|
|
15 |
* [Collections](#collections)
|
|
|
16 |
* [resolve()](#resolve)
|
|
|
17 |
* [Resolve cancellation](#resolve-cancellation)
|
|
|
18 |
* [reject()](#reject)
|
|
|
19 |
* [Reject cancellation](#reject-cancellation)
|
|
|
20 |
* [TimeoutException](#timeoutexception)
|
|
|
21 |
* [Install](#install)
|
|
|
22 |
* [Tests](#tests)
|
|
|
23 |
* [License](#license)
|
|
|
24 |
|
|
|
25 |
## Usage
|
|
|
26 |
|
|
|
27 |
This lightweight library consists only of a few simple functions.
|
|
|
28 |
All functions reside under the `React\Promise\Timer` namespace.
|
|
|
29 |
|
|
|
30 |
The below examples assume you use an import statement similar to this:
|
|
|
31 |
|
|
|
32 |
```php
|
|
|
33 |
use React\Promise\Timer;
|
|
|
34 |
|
|
|
35 |
Timer\timeout(…);
|
|
|
36 |
```
|
|
|
37 |
|
|
|
38 |
Alternatively, you can also refer to them with their fully-qualified name:
|
|
|
39 |
|
|
|
40 |
```php
|
|
|
41 |
\React\Promise\Timer\timeout(…);
|
|
|
42 |
```
|
|
|
43 |
|
|
|
44 |
### timeout()
|
|
|
45 |
|
|
|
46 |
The `timeout(PromiseInterface $promise, $time, LoopInterface $loop)` function
|
|
|
47 |
can be used to *cancel* operations that take *too long*.
|
|
|
48 |
You need to pass in an input `$promise` that represents a pending operation and timeout parameters.
|
|
|
49 |
It returns a new `Promise` with the following resolution behavior:
|
|
|
50 |
|
|
|
51 |
* If the input `$promise` resolves before `$time` seconds, resolve the resulting promise with its fulfillment value.
|
|
|
52 |
* If the input `$promise` rejects before `$time` seconds, reject the resulting promise with its rejection value.
|
|
|
53 |
* If the input `$promise` does not settle before `$time` seconds, *cancel* the operation and reject the resulting promise with a [`TimeoutException`](#timeoutexception).
|
|
|
54 |
|
|
|
55 |
Internally, the given `$time` value will be used to start a timer that will
|
|
|
56 |
*cancel* the pending operation once it triggers.
|
|
|
57 |
This implies that if you pass a really small (or negative) value, it will still
|
|
|
58 |
start a timer and will thus trigger at the earliest possible time in the future.
|
|
|
59 |
|
|
|
60 |
If the input `$promise` is already settled, then the resulting promise will
|
|
|
61 |
resolve or reject immediately without starting a timer at all.
|
|
|
62 |
|
|
|
63 |
A common use case for handling only resolved values looks like this:
|
|
|
64 |
|
|
|
65 |
```php
|
|
|
66 |
$promise = accessSomeRemoteResource();
|
|
|
67 |
Timer\timeout($promise, 10.0, $loop)->then(function ($value) {
|
|
|
68 |
// the operation finished within 10.0 seconds
|
|
|
69 |
});
|
|
|
70 |
```
|
|
|
71 |
|
|
|
72 |
A more complete example could look like this:
|
|
|
73 |
|
|
|
74 |
```php
|
|
|
75 |
$promise = accessSomeRemoteResource();
|
|
|
76 |
Timer\timeout($promise, 10.0, $loop)->then(
|
|
|
77 |
function ($value) {
|
|
|
78 |
// the operation finished within 10.0 seconds
|
|
|
79 |
},
|
|
|
80 |
function ($error) {
|
|
|
81 |
if ($error instanceof Timer\TimeoutException) {
|
|
|
82 |
// the operation has failed due to a timeout
|
|
|
83 |
} else {
|
|
|
84 |
// the input operation has failed due to some other error
|
|
|
85 |
}
|
|
|
86 |
}
|
|
|
87 |
);
|
|
|
88 |
```
|
|
|
89 |
|
|
|
90 |
Or if you're using [react/promise v2.2.0](https://github.com/reactphp/promise) or up:
|
|
|
91 |
|
|
|
92 |
```php
|
|
|
93 |
Timer\timeout($promise, 10.0, $loop)
|
|
|
94 |
->then(function ($value) {
|
|
|
95 |
// the operation finished within 10.0 seconds
|
|
|
96 |
})
|
|
|
97 |
->otherwise(function (Timer\TimeoutException $error) {
|
|
|
98 |
// the operation has failed due to a timeout
|
|
|
99 |
})
|
|
|
100 |
->otherwise(function ($error) {
|
|
|
101 |
// the input operation has failed due to some other error
|
|
|
102 |
})
|
|
|
103 |
;
|
|
|
104 |
```
|
|
|
105 |
|
|
|
106 |
#### Timeout cancellation
|
|
|
107 |
|
|
|
108 |
As discussed above, the [`timeout()`](#timeout) function will *cancel* the
|
|
|
109 |
underlying operation if it takes *too long*.
|
|
|
110 |
This means that you can be sure the resulting promise will then be rejected
|
|
|
111 |
with a [`TimeoutException`](#timeoutexception).
|
|
|
112 |
|
|
|
113 |
However, what happens to the underlying input `$promise` is a bit more tricky:
|
|
|
114 |
Once the timer fires, we will try to call
|
|
|
115 |
[`$promise->cancel()`](https://github.com/reactphp/promise#cancellablepromiseinterfacecancel)
|
|
|
116 |
on the input `$promise` which in turn invokes its [cancellation handler](#cancellation-handler).
|
|
|
117 |
|
|
|
118 |
This means that it's actually up the input `$promise` to handle
|
|
|
119 |
[cancellation support](https://github.com/reactphp/promise#cancellablepromiseinterface).
|
|
|
120 |
|
|
|
121 |
* A common use case involves cleaning up any resources like open network sockets or
|
|
|
122 |
file handles or terminating external processes or timers.
|
|
|
123 |
|
|
|
124 |
* If the given input `$promise` does not support cancellation, then this is a NO-OP.
|
|
|
125 |
This means that while the resulting promise will still be rejected, the underlying
|
|
|
126 |
input `$promise` may still be pending and can hence continue consuming resources.
|
|
|
127 |
|
|
|
128 |
See the following chapter for more details on the cancellation handler.
|
|
|
129 |
|
|
|
130 |
#### Cancellation handler
|
|
|
131 |
|
|
|
132 |
For example, an implementation for the above operation could look like this:
|
|
|
133 |
|
|
|
134 |
```php
|
|
|
135 |
function accessSomeRemoteResource()
|
|
|
136 |
{
|
|
|
137 |
return new Promise(
|
|
|
138 |
function ($resolve, $reject) use (&$socket) {
|
|
|
139 |
// this will be called once the promise is created
|
|
|
140 |
// a common use case involves opening any resources and eventually resolving
|
|
|
141 |
$socket = createSocket();
|
|
|
142 |
$socket->on('data', function ($data) use ($resolve) {
|
|
|
143 |
$resolve($data);
|
|
|
144 |
});
|
|
|
145 |
},
|
|
|
146 |
function ($resolve, $reject) use (&$socket) {
|
|
|
147 |
// this will be called once calling `cancel()` on this promise
|
|
|
148 |
// a common use case involves cleaning any resources and then rejecting
|
|
|
149 |
$socket->close();
|
|
|
150 |
$reject(new \RuntimeException('Operation cancelled'));
|
|
|
151 |
}
|
|
|
152 |
);
|
|
|
153 |
}
|
|
|
154 |
```
|
|
|
155 |
|
|
|
156 |
In this example, calling `$promise->cancel()` will invoke the registered cancellation
|
|
|
157 |
handler which then closes the network socket and rejects the `Promise` instance.
|
|
|
158 |
|
|
|
159 |
If no cancellation handler is passed to the `Promise` constructor, then invoking
|
|
|
160 |
its `cancel()` method it is effectively a NO-OP.
|
|
|
161 |
This means that it may still be pending and can hence continue consuming resources.
|
|
|
162 |
|
|
|
163 |
For more details on the promise cancellation, please refer to the
|
|
|
164 |
[Promise documentation](https://github.com/reactphp/promise#cancellablepromiseinterface).
|
|
|
165 |
|
|
|
166 |
#### Input cancellation
|
|
|
167 |
|
|
|
168 |
Irrespective of the timeout handling, you can also explicitly `cancel()` the
|
|
|
169 |
input `$promise` at any time.
|
|
|
170 |
This means that the `timeout()` handling does not affect cancellation of the
|
|
|
171 |
input `$promise`, as demonstrated in the following example:
|
|
|
172 |
|
|
|
173 |
```php
|
|
|
174 |
$promise = accessSomeRemoteResource();
|
|
|
175 |
$timeout = Timer\timeout($promise, 10.0, $loop);
|
|
|
176 |
|
|
|
177 |
$promise->cancel();
|
|
|
178 |
```
|
|
|
179 |
|
|
|
180 |
The registered [cancellation handler](#cancellation-handler) is responsible for
|
|
|
181 |
handling the `cancel()` call:
|
|
|
182 |
|
|
|
183 |
* A described above, a common use involves resource cleanup and will then *reject*
|
|
|
184 |
the `Promise`.
|
|
|
185 |
If the input `$promise` is being rejected, then the timeout will be aborted
|
|
|
186 |
and the resulting promise will also be rejected.
|
|
|
187 |
* If the input `$promise` is still pending, then the timout will continue
|
|
|
188 |
running until the timer expires.
|
|
|
189 |
The same happens if the input `$promise` does not register a
|
|
|
190 |
[cancellation handler](#cancellation-handler).
|
|
|
191 |
|
|
|
192 |
#### Output cancellation
|
|
|
193 |
|
|
|
194 |
Similarily, you can also explicitly `cancel()` the resulting promise like this:
|
|
|
195 |
|
|
|
196 |
```php
|
|
|
197 |
$promise = accessSomeRemoteResource();
|
|
|
198 |
$timeout = Timer\timeout($promise, 10.0, $loop);
|
|
|
199 |
|
|
|
200 |
$timeout->cancel();
|
|
|
201 |
```
|
|
|
202 |
|
|
|
203 |
Note how this looks very similar to the above [input cancellation](#input-cancellation)
|
|
|
204 |
example. Accordingly, it also behaves very similar.
|
|
|
205 |
|
|
|
206 |
Calling `cancel()` on the resulting promise will merely try
|
|
|
207 |
to `cancel()` the input `$promise`.
|
|
|
208 |
This means that we do not take over responsibility of the outcome and it's
|
|
|
209 |
entirely up to the input `$promise` to handle cancellation support.
|
|
|
210 |
|
|
|
211 |
The registered [cancellation handler](#cancellation-handler) is responsible for
|
|
|
212 |
handling the `cancel()` call:
|
|
|
213 |
|
|
|
214 |
* As described above, a common use involves resource cleanup and will then *reject*
|
|
|
215 |
the `Promise`.
|
|
|
216 |
If the input `$promise` is being rejected, then the timeout will be aborted
|
|
|
217 |
and the resulting promise will also be rejected.
|
|
|
218 |
* If the input `$promise` is still pending, then the timout will continue
|
|
|
219 |
running until the timer expires.
|
|
|
220 |
The same happens if the input `$promise` does not register a
|
|
|
221 |
[cancellation handler](#cancellation-handler).
|
|
|
222 |
|
|
|
223 |
To re-iterate, note that calling `cancel()` on the resulting promise will merely
|
|
|
224 |
try to cancel the input `$promise` only.
|
|
|
225 |
It is then up to the cancellation handler of the input promise to settle the promise.
|
|
|
226 |
If the input promise is still pending when the timeout occurs, then the normal
|
|
|
227 |
[timeout cancellation](#timeout-cancellation) handling will trigger, effectively rejecting
|
|
|
228 |
the output promise with a [`TimeoutException`](#timeoutexception).
|
|
|
229 |
|
|
|
230 |
This is done for consistency with the [timeout cancellation](#timeout-cancellation)
|
|
|
231 |
handling and also because it is assumed this is often used like this:
|
|
|
232 |
|
|
|
233 |
```php
|
|
|
234 |
$timeout = Timer\timeout(accessSomeRemoteResource(), 10.0, $loop);
|
|
|
235 |
|
|
|
236 |
$timeout->cancel();
|
|
|
237 |
```
|
|
|
238 |
|
|
|
239 |
As described above, this example works as expected and cleans up any resources
|
|
|
240 |
allocated for the input `$promise`.
|
|
|
241 |
|
|
|
242 |
Note that if the given input `$promise` does not support cancellation, then this
|
|
|
243 |
is a NO-OP.
|
|
|
244 |
This means that while the resulting promise will still be rejected after the
|
|
|
245 |
timeout, the underlying input `$promise` may still be pending and can hence
|
|
|
246 |
continue consuming resources.
|
|
|
247 |
|
|
|
248 |
#### Collections
|
|
|
249 |
|
|
|
250 |
If you want to wait for multiple promises to resolve, you can use the normal promise primitives like this:
|
|
|
251 |
|
|
|
252 |
```php
|
|
|
253 |
$promises = array(
|
|
|
254 |
accessSomeRemoteResource(),
|
|
|
255 |
accessSomeRemoteResource(),
|
|
|
256 |
accessSomeRemoteResource()
|
|
|
257 |
);
|
|
|
258 |
|
|
|
259 |
$promise = \React\Promise\all($promises);
|
|
|
260 |
|
|
|
261 |
Timer\timeout($promise, 10, $loop)->then(function ($values) {
|
|
|
262 |
// *all* promises resolved
|
|
|
263 |
});
|
|
|
264 |
```
|
|
|
265 |
|
|
|
266 |
The applies to all promise collection primitives alike, i.e. `all()`, `race()`, `any()`, `some()` etc.
|
|
|
267 |
|
|
|
268 |
For more details on the promise primitives, please refer to the
|
|
|
269 |
[Promise documentation](https://github.com/reactphp/promise#functions).
|
|
|
270 |
|
|
|
271 |
### resolve()
|
|
|
272 |
|
|
|
273 |
The `resolve($time, LoopInterface $loop)` function can be used to create a new Promise that
|
|
|
274 |
resolves in `$time` seconds with the `$time` as the fulfillment value.
|
|
|
275 |
|
|
|
276 |
```php
|
|
|
277 |
Timer\resolve(1.5, $loop)->then(function ($time) {
|
|
|
278 |
echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL;
|
|
|
279 |
});
|
|
|
280 |
```
|
|
|
281 |
|
|
|
282 |
Internally, the given `$time` value will be used to start a timer that will
|
|
|
283 |
resolve the promise once it triggers.
|
|
|
284 |
This implies that if you pass a really small (or negative) value, it will still
|
|
|
285 |
start a timer and will thus trigger at the earliest possible time in the future.
|
|
|
286 |
|
|
|
287 |
#### Resolve cancellation
|
|
|
288 |
|
|
|
289 |
You can explicitly `cancel()` the resulting timer promise at any time:
|
|
|
290 |
|
|
|
291 |
```php
|
|
|
292 |
$timer = Timer\resolve(2.0, $loop);
|
|
|
293 |
|
|
|
294 |
$timer->cancel();
|
|
|
295 |
```
|
|
|
296 |
|
|
|
297 |
This will abort the timer and *reject* with a `RuntimeException`.
|
|
|
298 |
|
|
|
299 |
### reject()
|
|
|
300 |
|
|
|
301 |
The `reject($time, LoopInterface $loop)` function can be used to create a new Promise
|
|
|
302 |
which rejects in `$time` seconds with a `TimeoutException`.
|
|
|
303 |
|
|
|
304 |
```php
|
|
|
305 |
Timer\reject(2.0, $loop)->then(null, function (TimeoutException $e) {
|
|
|
306 |
echo 'Rejected after ' . $e->getTimeout() . ' seconds ' . PHP_EOL;
|
|
|
307 |
});
|
|
|
308 |
```
|
|
|
309 |
|
|
|
310 |
Internally, the given `$time` value will be used to start a timer that will
|
|
|
311 |
reject the promise once it triggers.
|
|
|
312 |
This implies that if you pass a really small (or negative) value, it will still
|
|
|
313 |
start a timer and will thus trigger at the earliest possible time in the future.
|
|
|
314 |
|
|
|
315 |
This function complements the [`resolve()`](#resolve) function
|
|
|
316 |
and can be used as a basic building block for higher-level promise consumers.
|
|
|
317 |
|
|
|
318 |
#### Reject cancellation
|
|
|
319 |
|
|
|
320 |
You can explicitly `cancel()` the resulting timer promise at any time:
|
|
|
321 |
|
|
|
322 |
```php
|
|
|
323 |
$timer = Timer\reject(2.0, $loop);
|
|
|
324 |
|
|
|
325 |
$timer->cancel();
|
|
|
326 |
```
|
|
|
327 |
|
|
|
328 |
This will abort the timer and *reject* with a `RuntimeException`.
|
|
|
329 |
|
|
|
330 |
### TimeoutException
|
|
|
331 |
|
|
|
332 |
The `TimeoutException` extends PHP's built-in `RuntimeException`.
|
|
|
333 |
|
|
|
334 |
The `getTimeout()` method can be used to get the timeout value in seconds.
|
|
|
335 |
|
|
|
336 |
## Install
|
|
|
337 |
|
|
|
338 |
The recommended way to install this library is [through Composer](https://getcomposer.org).
|
|
|
339 |
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
|
|
340 |
|
|
|
341 |
This project follows [SemVer](https://semver.org/).
|
|
|
342 |
This will install the latest supported version:
|
|
|
343 |
|
|
|
344 |
```bash
|
|
|
345 |
$ composer require react/promise-timer:^1.6
|
|
|
346 |
```
|
|
|
347 |
|
|
|
348 |
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
|
|
349 |
|
|
|
350 |
This project aims to run on any platform and thus does not require any PHP
|
|
|
351 |
extensions and supports running on legacy PHP 5.3 through current PHP 7+ and
|
|
|
352 |
HHVM.
|
|
|
353 |
It's *highly recommended to use PHP 7+* for this project.
|
|
|
354 |
|
|
|
355 |
## Tests
|
|
|
356 |
|
|
|
357 |
To run the test suite, you first need to clone this repo and then install all
|
|
|
358 |
dependencies [through Composer](https://getcomposer.org):
|
|
|
359 |
|
|
|
360 |
```bash
|
|
|
361 |
$ composer install
|
|
|
362 |
```
|
|
|
363 |
|
|
|
364 |
To run the test suite, go to the project root and run:
|
|
|
365 |
|
|
|
366 |
```bash
|
|
|
367 |
$ php vendor/bin/phpunit
|
|
|
368 |
```
|
|
|
369 |
|
|
|
370 |
## License
|
|
|
371 |
|
|
|
372 |
MIT, see [LICENSE file](LICENSE).
|