ReactPHP: Timers
One of ReactPHP's simplest feature is timers. They are a core functionality exposed by the event-loop
. Timers are an incredible powerful yet simple feature that can get you into trouble if you don't watch out. During this post we'll look at all different kind of interactions you can do with timers.
Installation
Since timers are part of the event-loop
installing them is as simple as installing the event-loop
. (Don't worry we're not going to dive into the details of the event-loop
in this post, that post comes when we've gone over all the event-loop
features.) We're only use timers for now.
composer require react/event-loop
At the beginning of each example you'll notice:
$loop = React\EventLoop\Factory::create();
That is a shortcut for creating the best available event-loop
. And at the end you'll notice:
$loop->run();
That runs the event-loop
. All code before that is just setting it up, actually execution will start then.
Methods
The event-loop
exposes the following methods for creating and interacting with timers:
- addTimer - one-off timer (setTimeout in JavaScript)
- addPeriodicTimer - timer at a set interval (setInterval in JavaScript)
- cancelTimer - stops a running timer be (clearTimeout/clearInterval in JavaScript)
- isTimerActive - check whether the timer is still active or not
A simple timer
Lets start with a simple increment example:
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$i = 0;
$loop->addPeriodicTimer(1, function() use (&$i) {
echo ++$i, PHP_EOL;
});
$loop->run();
Looks pretty simple right. We've set up the event-loop
, attach a timer
to it and started the event-loop
. Then every second it will increment and echo $i
.
Keeping control over your timer
Now the example above will run indefinitely until you kill it. We prefer control over the tools we create. Otherwise they will turn into time bombs. So we’re adding a check that cancels the timer once $i
equals 15. First we need the Timer
instance, which is passed into our closure by default.
$loop->addPeriodicTimer(1, function(React\EventLoop\Timer\Timer $timer) use (&$i, $loop) {
Secondly we check each time if $i
equals 15 or more and cancel the timer
if it has. We add the following to our closure body after the echo:
if ($i >= 15) {
$loop->cancelTimer($timer);
}
Ending up with this bit of code:
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$i = 0;
$loop->addPeriodicTimer(1, function(React\EventLoop\Timer\Timer $timer) use (&$i, $loop) {
echo ++$i, PHP_EOL;
if ($i >= 15) {
$loop->cancelTimer($timer);
}
});
$loop->run();
Running this the program will output 1 to 15 and then cancel the timer. The event-loop
has nothing to do from that point on and stops.
Using a timer to stop another timer
The above works fine but isn't really flexible. In the next example we use a secondary timer to stop it after +/- 15 seconds. This is really handy if you want to set a timeout on something. (The community examples use this method.)
Consider the following example. It stores the first timer in $timer
and has a secondary timer that uses $timer
to cancel the first timer.
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$i = 0;
$timer = $loop->addPeriodicTimer(1, function() use (&$i) {
echo ++$i, PHP_EOL;
});
$loop->addTimer(15, function () use ($timer, $loop) {
$loop->cancelTimer($timer);
});
$loop->run();
Checking a timer
There are times when you don't know if a timer should be active or not. As mentioned at the beginning of the post there is a method to do that. It's rather simple to use, just pass a timer into it to find out if it's active or not: $loop->isTimerActive($timer)
.
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$i = 0;
$timer = $loop->addTimer(1, function() {
echo 'Timer done', PHP_EOL;
});
$loop->addTimer(3, function () use ($timer, $loop) {
if ($loop->isTimerActive($timer)) {
echo 'Timer active', PHP_EOL;
} else {
echo 'Timer inactive', PHP_EOL;
}
});
$loop->run();
Blocking
Beware that timers are not exact and accuracy varies. The reason for this is that the event-loop
also has others thing to do and those might block the timer execution for a few milliseconds or more depending on the loop load. (We'll take a closer look at how the event-loop
exactly works in a later post. Keeping it simple for now.) The example below shows what happens if we block the event-loop
hard with sleep.
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$i = 0;
$loop->addPeriodicTimer(1, function() use (&$i) {
echo ++$i, PHP_EOL;
});
$loop->addTimer(10, function () {
sleep(3);
});
$loop->run();
Community examples
Lets take a look at this weeks community examples predis/predis-async
and wyrihaximus/react-guzzle-ring
. Both use timers to setup connection and/or request timeouts.
predis/predis-async
predis/predis-async
is the asynchronous Redis client version of Predis. It uses a timer to handle connection timeouts. Internally it sets a timer when connecting to the redis server. When the connecting fails the internal state is reset and an exception is passed to the user. In normal circumstances the connection will be successful and the timer is cancelled.
From a users perspective you just connect to a Redis
server, the connection timeout is handled for you. Without that timeout you would never been notified about the fact you're still trying to connect. This leads to memory leaks and process instabilities etc etc.
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$client = new Predis\Async\Client('tcp://127.0.0.1:6379', $loop);
$client->connect(function ($client) {
echo "Connected to Redis, now listening for incoming messages...\n";
});
$loop->run();
wyrihaximus/react-guzzle-ring
wyrihaximus/react-guzzle-ring
is a RingPHP handler for Guzzle v5. Built upon react/http-client
it provides an asynchronous handler that lets the user use the performance of react with the power of Guzzle. Just as predis/predis-async
it uses a timer in a similar fashion to detect and handle connection timeouts.
Consider the following example. It sets up a request to amazon.com
and sets the request timeout at 0.001
second. Now amazon has some mighty powerful hardware but it is highly unlikely they can send a response body back in 0.001
second.
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use React\EventLoop\Factory;
use WyriHaximus\React\RingPHP\HttpClientAdapter;
$loop = Factory::create();
$guzzle = new Client([
'handler' => new HttpClientAdapter($loop),
]);
$guzzle->get('http://www.amazon.com/', [
'timeout' => 0.001,
'future' => true,
])->then(function() {
echo 'Amazon completed' . PHP_EOL;
}, function(exception $error) {
echo 'Amazon error' . PHP_EOL;
echo $error->getMessage() . PHP_EOL;
});
$loop->run();
Examples
All the examples from this post can be found on Github.
Conclusion
Timers are a really simple but neat piece of ReactPHP that lets you keep an eye on your code. Despite the simplicity it's rather powerful and useful when dealing with connections and other timer sensitive code.