Skip to content

Commit 4ba58fb

Browse files
authored
Merge pull request #69 from clue-labs/delay
Add new `delay()` function to delay program execution
2 parents 34c49a1 + 608a67c commit 4ba58fb

File tree

3 files changed

+325
-9
lines changed

3 files changed

+325
-9
lines changed

README.md

+108-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ an event loop, it can be used with this library.
2020
* [async()](#async)
2121
* [await()](#await)
2222
* [coroutine()](#coroutine)
23+
* [delay()](#delay)
2324
* [parallel()](#parallel)
2425
* [series()](#series)
2526
* [waterfall()](#waterfall)
@@ -202,17 +203,16 @@ $promise->then(function (int $bytes) {
202203
The returned promise is implemented in such a way that it can be cancelled
203204
when it is still pending. Cancelling a pending promise will cancel any awaited
204205
promises inside that fiber or any nested fibers. As such, the following example
205-
will only output `ab` and cancel the pending [`sleep()`](https://reactphp.org/promise-timer/#sleep).
206+
will only output `ab` and cancel the pending [`delay()`](#delay).
206207
The [`await()`](#await) calls in this example would throw a `RuntimeException`
207-
from the cancelled [`sleep()`](https://reactphp.org/promise-timer/#sleep) call
208-
that bubbles up through the fibers.
208+
from the cancelled [`delay()`](#delay) call that bubbles up through the fibers.
209209

210210
```php
211211
$promise = async(static function (): int {
212212
echo 'a';
213213
await(async(static function (): void {
214214
echo 'b';
215-
await(React\Promise\Timer\sleep(2));
215+
delay(2);
216216
echo 'c';
217217
})());
218218
echo 'd';
@@ -392,6 +392,110 @@ $promise->then(function (int $bytes) {
392392
});
393393
```
394394

395+
## delay()
396+
397+
The `delay(float $seconds): void` function can be used to
398+
delay program execution for duration given in `$seconds`.
399+
400+
```php
401+
React\Async\delay($seconds);
402+
```
403+
404+
This function will only return after the given number of `$seconds` have
405+
elapsed. If there are no other events attached to this loop, it will behave
406+
similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).
407+
408+
```php
409+
echo 'a';
410+
React\Async\delay(1.0);
411+
echo 'b';
412+
413+
// prints "a" at t=0.0s
414+
// prints "b" at t=1.0s
415+
```
416+
417+
Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
418+
this function may not necessarily halt execution of the entire process thread.
419+
Instead, it allows the event loop to run any other events attached to the
420+
same loop until the delay returns:
421+
422+
```php
423+
echo 'a';
424+
Loop::addTimer(1.0, function () {
425+
echo 'b';
426+
});
427+
React\Async\delay(3.0);
428+
echo 'c';
429+
430+
// prints "a" at t=0.0s
431+
// prints "b" at t=1.0s
432+
// prints "c" at t=3.0s
433+
```
434+
435+
This behavior is especially useful if you want to delay the program execution
436+
of a particular routine, such as when building a simple polling or retry
437+
mechanism:
438+
439+
```php
440+
try {
441+
something();
442+
} catch (Throwable) {
443+
// in case of error, retry after a short delay
444+
React\Async\delay(1.0);
445+
something();
446+
}
447+
```
448+
449+
Because this function only returns after some time has passed, it can be
450+
considered *blocking* from the perspective of the calling code. You can avoid
451+
this blocking behavior by wrapping it in an [`async()` function](#async) call.
452+
Everything inside this function will still be blocked, but everything outside
453+
this function can be executed asynchronously without blocking:
454+
455+
```php
456+
Loop::addTimer(0.5, React\Async\async(function () {
457+
echo 'a';
458+
React\Async\delay(1.0);
459+
echo 'c';
460+
}));
461+
462+
Loop::addTimer(1.0, function () {
463+
echo 'b';
464+
});
465+
466+
// prints "a" at t=0.5s
467+
// prints "b" at t=1.0s
468+
// prints "c" at t=1.5s
469+
```
470+
471+
See also the [`async()` function](#async) for more details.
472+
473+
Internally, the `$seconds` argument will be used as a timer for the loop so that
474+
it keeps running until this timer triggers. This implies that if you pass a
475+
really small (or negative) value, it will still start a timer and will thus
476+
trigger at the earliest possible time in the future.
477+
478+
The function is implemented in such a way that it can be cancelled when it is
479+
running inside an [`async()` function](#async). Cancelling the resulting
480+
promise will clean up any pending timers and throw a `RuntimeException` from
481+
the pending delay which in turn would reject the resulting promise.
482+
483+
```php
484+
$promise = async(function () {
485+
echo 'a';
486+
delay(3.0);
487+
echo 'b';
488+
});
489+
490+
Loop::addTimer(2.0, function () use ($promise) {
491+
$promise->cancel();
492+
});
493+
494+
// prints "a" at t=0.0s
495+
// rejects $promise at t=2.0
496+
// never prints "b"
497+
```
498+
395499
### parallel()
396500

397501
The `parallel(iterable<callable():PromiseInterface<mixed,Exception>> $tasks): PromiseInterface<array<mixed>,Exception>` function can be used

src/functions.php

+126-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace React\Async;
44

5+
use React\EventLoop\Loop;
6+
use React\EventLoop\TimerInterface;
57
use React\Promise\Deferred;
68
use React\Promise\Promise;
79
use React\Promise\PromiseInterface;
@@ -153,17 +155,16 @@
153155
* The returned promise is implemented in such a way that it can be cancelled
154156
* when it is still pending. Cancelling a pending promise will cancel any awaited
155157
* promises inside that fiber or any nested fibers. As such, the following example
156-
* will only output `ab` and cancel the pending [`sleep()`](https://reactphp.org/promise-timer/#sleep).
158+
* will only output `ab` and cancel the pending [`delay()`](#delay).
157159
* The [`await()`](#await) calls in this example would throw a `RuntimeException`
158-
* from the cancelled [`sleep()`](https://reactphp.org/promise-timer/#sleep) call
159-
* that bubbles up through the fibers.
160+
* from the cancelled [`delay()`](#delay) call that bubbles up through the fibers.
160161
*
161162
* ```php
162163
* $promise = async(static function (): int {
163164
* echo 'a';
164165
* await(async(static function (): void {
165166
* echo 'b';
166-
* await(React\Promise\Timer\sleep(2));
167+
* delay(2);
167168
* echo 'c';
168169
* })());
169170
* echo 'd';
@@ -215,7 +216,6 @@ function async(callable $function): callable
215216
};
216217
}
217218

218-
219219
/**
220220
* Block waiting for the given `$promise` to be fulfilled.
221221
*
@@ -352,6 +352,127 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL
352352
return $fiber->suspend();
353353
}
354354

355+
/**
356+
* Delay program execution for duration given in `$seconds`.
357+
*
358+
* ```php
359+
* React\Async\delay($seconds);
360+
* ```
361+
*
362+
* This function will only return after the given number of `$seconds` have
363+
* elapsed. If there are no other events attached to this loop, it will behave
364+
* similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).
365+
*
366+
* ```php
367+
* echo 'a';
368+
* React\Async\delay(1.0);
369+
* echo 'b';
370+
*
371+
* // prints "a" at t=0.0s
372+
* // prints "b" at t=1.0s
373+
* ```
374+
*
375+
* Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
376+
* this function may not necessarily halt execution of the entire process thread.
377+
* Instead, it allows the event loop to run any other events attached to the
378+
* same loop until the delay returns:
379+
*
380+
* ```php
381+
* echo 'a';
382+
* Loop::addTimer(1.0, function () {
383+
* echo 'b';
384+
* });
385+
* React\Async\delay(3.0);
386+
* echo 'c';
387+
*
388+
* // prints "a" at t=0.0s
389+
* // prints "b" at t=1.0s
390+
* // prints "c" at t=3.0s
391+
* ```
392+
*
393+
* This behavior is especially useful if you want to delay the program execution
394+
* of a particular routine, such as when building a simple polling or retry
395+
* mechanism:
396+
*
397+
* ```php
398+
* try {
399+
* something();
400+
* } catch (Throwable) {
401+
* // in case of error, retry after a short delay
402+
* React\Async\delay(1.0);
403+
* something();
404+
* }
405+
* ```
406+
*
407+
* Because this function only returns after some time has passed, it can be
408+
* considered *blocking* from the perspective of the calling code. You can avoid
409+
* this blocking behavior by wrapping it in an [`async()` function](#async) call.
410+
* Everything inside this function will still be blocked, but everything outside
411+
* this function can be executed asynchronously without blocking:
412+
*
413+
* ```php
414+
* Loop::addTimer(0.5, React\Async\async(function () {
415+
* echo 'a';
416+
* React\Async\delay(1.0);
417+
* echo 'c';
418+
* }));
419+
*
420+
* Loop::addTimer(1.0, function () {
421+
* echo 'b';
422+
* });
423+
*
424+
* // prints "a" at t=0.5s
425+
* // prints "b" at t=1.0s
426+
* // prints "c" at t=1.5s
427+
* ```
428+
*
429+
* See also the [`async()` function](#async) for more details.
430+
*
431+
* Internally, the `$seconds` argument will be used as a timer for the loop so that
432+
* it keeps running until this timer triggers. This implies that if you pass a
433+
* really small (or negative) value, it will still start a timer and will thus
434+
* trigger at the earliest possible time in the future.
435+
*
436+
* The function is implemented in such a way that it can be cancelled when it is
437+
* running inside an [`async()` function](#async). Cancelling the resulting
438+
* promise will clean up any pending timers and throw a `RuntimeException` from
439+
* the pending delay which in turn would reject the resulting promise.
440+
*
441+
* ```php
442+
* $promise = async(function () {
443+
* echo 'a';
444+
* delay(3.0);
445+
* echo 'b';
446+
* });
447+
*
448+
* Loop::addTimer(2.0, function () use ($promise) {
449+
* $promise->cancel();
450+
* });
451+
*
452+
* // prints "a" at t=0.0s
453+
* // rejects $promise at t=2.0
454+
* // never prints "b"
455+
* ```
456+
*
457+
* @return void
458+
* @throws \RuntimeException when the function is cancelled inside an `async()` function
459+
* @see async()
460+
* @uses await()
461+
*/
462+
function delay(float $seconds): void
463+
{
464+
/** @var ?TimerInterface $timer */
465+
$timer = null;
466+
467+
await(new Promise(function (callable $resolve) use ($seconds, &$timer): void {
468+
$timer = Loop::addTimer($seconds, fn() => $resolve(null));
469+
}, function () use (&$timer): void {
470+
assert($timer instanceof TimerInterface);
471+
Loop::cancelTimer($timer);
472+
throw new \RuntimeException('Delay cancelled');
473+
}));
474+
}
475+
355476
/**
356477
* Execute a Generator-based coroutine to "await" promises.
357478
*

tests/DelayTest.php

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace React\Tests\Async;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use React\EventLoop\Loop;
7+
use function React\Async\async;
8+
use function React\Async\await;
9+
use function React\Async\delay;
10+
11+
class DelayTest extends TestCase
12+
{
13+
public function testDelayBlocksForGivenPeriod()
14+
{
15+
$time = microtime(true);
16+
delay(0.02);
17+
$time = microtime(true) - $time;
18+
19+
$this->assertGreaterThan(0.01, $time);
20+
$this->assertLessThan(0.03, $time);
21+
}
22+
23+
public function testDelaySmallPeriodBlocksForCloseToZeroSeconds()
24+
{
25+
$time = microtime(true);
26+
delay(0.000001);
27+
$time = microtime(true) - $time;
28+
29+
$this->assertLessThan(0.01, $time);
30+
}
31+
32+
public function testDelayNegativePeriodBlocksForCloseToZeroSeconds()
33+
{
34+
$time = microtime(true);
35+
delay(-1);
36+
$time = microtime(true) - $time;
37+
38+
$this->assertLessThan(0.01, $time);
39+
}
40+
41+
public function testAwaitAsyncDelayBlocksForGivenPeriod()
42+
{
43+
$promise = async(function () {
44+
delay(0.02);
45+
})();
46+
47+
$time = microtime(true);
48+
await($promise);
49+
$time = microtime(true) - $time;
50+
51+
$this->assertGreaterThan(0.01, $time);
52+
$this->assertLessThan(0.03, $time);
53+
}
54+
55+
public function testAwaitAsyncDelayCancelledImmediatelyStopsTimerAndBlocksForCloseToZeroSeconds()
56+
{
57+
$promise = async(function () {
58+
delay(1.0);
59+
})();
60+
$promise->cancel();
61+
62+
$time = microtime(true);
63+
try {
64+
await($promise);
65+
} catch (\RuntimeException $e) {
66+
$this->assertEquals('Delay cancelled', $e->getMessage());
67+
}
68+
$time = microtime(true) - $time;
69+
70+
$this->assertLessThan(0.03, $time);
71+
}
72+
73+
public function testAwaitAsyncDelayCancelledAfterSmallPeriodStopsTimerAndBlocksUntilCancelled()
74+
{
75+
$promise = async(function () {
76+
delay(1.0);
77+
})();
78+
Loop::addTimer(0.02, fn() => $promise->cancel());
79+
80+
$time = microtime(true);
81+
try {
82+
await($promise);
83+
} catch (\RuntimeException $e) {
84+
$this->assertEquals('Delay cancelled', $e->getMessage());
85+
}
86+
$time = microtime(true) - $time;
87+
88+
$this->assertGreaterThan(0.01, $time);
89+
$this->assertLessThan(0.03, $time);
90+
}
91+
}

0 commit comments

Comments
 (0)