Skip to content

[2.x] Add new delay() function to delay program execution #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ an event loop, it can be used with this library.

* [Usage](#usage)
* [await()](#await)
* [delay()](#delay)
* [parallel()](#parallel)
* [series()](#series)
* [waterfall()](#waterfall)
Expand Down Expand Up @@ -90,6 +91,74 @@ try {
}
```

### delay()

The `delay(float $seconds): void` function can be used to
delay program execution for duration given in `$seconds`.

```php
React\Async\delay($seconds);
```

This function will only return after the given number of `$seconds` have
elapsed. If there are no other events attached to this loop, it will behave
similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).

```php
echo 'a';
React\Async\delay(1.0);
echo 'b';

// prints "a" at t=0.0s
// prints "b" at t=1.0s
```

Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
this function may not necessarily halt execution of the entire process thread.
Instead, it allows the event loop to run any other events attached to the
same loop until the delay returns:

```php
echo 'a';
Loop::addTimer(1.0, function () {
echo 'b';
});
React\Async\delay(3.0);
echo 'c';

// prints "a" at t=0.0s
// prints "b" at t=1.0s
// prints "c" at t=3.0s
```

This behavior is especially useful if you want to delay the program execution
of a particular routine, such as when building a simple polling or retry
mechanism:

```php
try {
something();
} catch (Exception $e) {
// in case of error, retry after a short delay
React\Async\delay(1.0);
something();
}
```

Because this function only returns after some time has passed, it can be
considered *blocking* from the perspective of the calling code. While the
delay is running, this function will assume control over the event loop.
Internally, it will `run()` the [default loop](https://github.com/reactphp/event-loop#loop)
until the delay returns and then calls `stop()` to terminate execution of the
loop. This means this function is more suited for short-lived promise executions
when using promise-based APIs is not feasible. For long-running applications,
using promise-based APIs by leveraging chained `then()` calls is usually preferable.

Internally, the `$seconds` argument will be used as a timer for the loop so that
it keeps running until this timer triggers. This implies that if you pass a
really small (or negative) value, it will still start a timer and will thus
trigger at the earliest possible time in the future.

### parallel()

The `parallel(array<callable():PromiseInterface<mixed,Exception>> $tasks): PromiseInterface<array<mixed>,Exception>` function can be used
Expand Down
80 changes: 80 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use React\EventLoop\Loop;
use React\Promise\Deferred;
use React\Promise\Promise;
use React\Promise\PromiseInterface;

/**
Expand Down Expand Up @@ -98,6 +99,85 @@ function ($error) use (&$exception, &$rejected, &$wait, &$loopStarted) {
return $resolved;
}

/**
* Delay program execution for duration given in `$seconds`.
*
* ```php
* React\Async\delay($seconds);
* ```
*
* This function will only return after the given number of `$seconds` have
* elapsed. If there are no other events attached to this loop, it will behave
* similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).
*
* ```php
* echo 'a';
* React\Async\delay(1.0);
* echo 'b';
*
* // prints "a" at t=0.0s
* // prints "b" at t=1.0s
* ```
*
* Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
* this function may not necessarily halt execution of the entire process thread.
* Instead, it allows the event loop to run any other events attached to the
* same loop until the delay returns:
*
* ```php
* echo 'a';
* Loop::addTimer(1.0, function () {
* echo 'b';
* });
* React\Async\delay(3.0);
* echo 'c';
*
* // prints "a" at t=0.0s
* // prints "b" at t=1.0s
* // prints "c" at t=3.0s
* ```
*
* This behavior is especially useful if you want to delay the program execution
* of a particular routine, such as when building a simple polling or retry
* mechanism:
*
* ```php
* try {
* something();
* } catch (Exception $e) {
* // in case of error, retry after a short delay
* React\Async\delay(1.0);
* something();
* }
* ```
*
* Because this function only returns after some time has passed, it can be
* considered *blocking* from the perspective of the calling code. While the
* delay is running, this function will assume control over the event loop.
* Internally, it will `run()` the [default loop](https://github.com/reactphp/event-loop#loop)
* until the delay returns and then calls `stop()` to terminate execution of the
* loop. This means this function is more suited for short-lived promise executions
* when using promise-based APIs is not feasible. For long-running applications,
* using promise-based APIs by leveraging chained `then()` calls is usually preferable.
*
* Internally, the `$seconds` argument will be used as a timer for the loop so that
* it keeps running until this timer triggers. This implies that if you pass a
* really small (or negative) value, it will still start a timer and will thus
* trigger at the earliest possible time in the future.
*
* @param float $seconds
* @return void
* @uses await()
*/
function delay($seconds)
{
await(new Promise(function ($resolve) use ($seconds) {
Loop::addTimer($seconds, function () use ($resolve) {
$resolve(null);
});
}));
}

/**
* @param array<callable():PromiseInterface<mixed,Exception>> $tasks
* @return PromiseInterface<array<mixed>,Exception>
Expand Down
55 changes: 55 additions & 0 deletions tests/DelayTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace React\Tests\Async;

use React;
use React\EventLoop\Loop;

class DelayTest extends TestCase
{
public function testDelayBlocksForGivenPeriod()
{
$time = microtime(true);
React\Async\delay(0.02);
$time = microtime(true) - $time;

if (method_exists($this, 'assertEqualsWithDelta')) {
// PHPUnit 7+
$this->assertEqualsWithDelta(0.02, $time, 0.01);
} else {
// legacy PHPUnit
$this->assertEquals(0.02, $time, '', 0.01);
}
}

public function testDelaySmallPeriodBlocksForCloseToZeroSeconds()
{
$time = microtime(true);
React\Async\delay(0.000001);
$time = microtime(true) - $time;

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

public function testDelayNegativePeriodBlocksForCloseToZeroSeconds()
{
$time = microtime(true);
React\Async\delay(-1);
$time = microtime(true) - $time;

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

public function testDelayRunsOtherEventsWhileWaiting()
{
$buffer = 'a';
Loop::addTimer(0.001, function () use (&$buffer) {
$buffer .= 'c';
});
$buffer .= 'b';
React\Async\delay(0.002);
$buffer .= 'd';

$this->assertEquals('abcd', $buffer);
}
}