Recoil is a generator-based cooperative multitasking kernel for React.
- Install via Composer package recoil/recoil
- Read the API documentation
The goal of Recoil is to enable development of asynchronous applications using familiar imperative programming techniques. The example below uses the Recoil Redis client and the React DNS component to resolve several domain names concurrently and store the results in a Redis database.
use React\Dns\Resolver\Factory as ResolverFactory;
use Recoil\Recoil;
use Recoil\Redis\Client as RedisClient;
/**
* Resolve a domain name and store the result in Redis.
*/
function resolveAndStore($redisClient, $dnsResolver, $domainName)
{
try {
$ipAddress = (yield $dnsResolver->resolve($domainName));
yield $redisClient->set($domainName, $ipAddress);
echo 'Resolved "' . $domainName . '" to ' . $ipAddress . PHP_EOL;
} catch (Exception $e) {
echo 'Failed to resolve "' . $domainName . '" - ' . $e->getMessage() . PHP_EOL;
}
}
Recoil::run(
function () {
$dnsResolver = (new ResolverFactory)->create(
'8.8.8.8',
(yield Recoil::eventLoop())
);
$redisClient = new RedisClient;
yield $redisClient->connect();
// Yielding an array of coroutines executes them concurrently.
yield [
resolveAndStore($redisClient, $dnsResolver, 'recoil.io'),
resolveAndStore($redisClient, $dnsResolver, 'reactphp.org'),
resolveAndStore($redisClient, $dnsResolver, 'icecave.com.au'),
];
yield $redisClient->disconnect();
}
);
Note that there is no callback-passing, and that regular PHP exceptions are used for reporting errors.
Recoil uses PHP generators to implement coroutines. Coroutines are functions that can be suspended and resumed while persisting contextual information such as local variables. By choosing to suspend execution of a coroutine at key points such as while waiting for I/O, asynchronous applications can be built to resemble traditional synchronous applications.
Nikita Popov has published an excellent article explaining the usage and benefits of generator-based coroutines. The article even includes an example implementation of a coroutine scheduler, though it takes a somewhat different approach.
Coroutines are units of work that can be suspended and resumed while maintaining execution state. Coroutines can produce values when suspended, and receive values or exceptions when resumed. Coroutines based on PHP generators are the basic building blocks of a Recoil application.
Strands provide a thread-like abstraction for coroutine execution in a Recoil application. Much like a thread provided by the operating system each strand has its own call stack and may be suspended, resumed, joined and terminated without affecting other strands.
Unlike threads, execution of a strand can only suspend or resume when a coroutine specifically requests to do so, hence the term cooperative multitasking.
Strands are very light-weight and are sometimes known as green threads.
The kernel is responsible for creating and scheduling strands, much like the operating system kernel does for threads. Internally, the kernel uses a React event-loop for scheduling. This allows applications to execute coroutine based code alongside "conventional" React code by sharing an event-loop instance.
Coroutine control flow, the current strand, and the kernel itself can be manipulated using the kernel API. The supported operations are defined in KernelApiInterface (though custom kernel implementations may provide additional operations). Inside an executing coroutine, the kernel API for the current kernel is accessed via the Recoil facade.
Streams provide a coroutine based abstraction for readable and writable data streams. The interfaces are somewhat similar to the built-in PHP stream API.
Stream operations are cooperative, that is, when reading or writing to a stream, execution of the coroutine is suspended until the stream is ready, allowing the kernel to schedule other strands for execution while waiting.
The stream-file example demonstrates using a readable stream to read a file.
Channels are stream-like objects that produce and consume PHP values rather than byte streams. Channels are intended as the primary method for communication between strands.
Like streams there are readable and writable variants. Some channel implementations allow for multiple concurrent read and write operations.
Both in-memory and stream-based channels are provided. Stream-based channels use a serialization protocol to encode and decode PHP values for transmission over a stream and as such can be useful for IPC or network communication.
The channel-ipc example demonstrates using stream-based channels to communicate with a sub-process.
The following examples illustrate the basic usage of coroutines and the kernel API. Additional examples are available in
the examples folder. References to the class Recoil
refer to the Recoil facade.
The following example shows the simplest way to execute a generator as a coroutine.
Recoil::run(
function () {
echo 'Hello, world!' . PHP_EOL;
yield Recoil::noop();
}
);
Recoil::run()
is a convenience method that instantiates a kernel and executes the given coroutine in a new strand.
Yielding Recoil::noop()
(no-operation) allows for the use of the yield
keyword - which forces PHP to parse the
function as a generator - without changing the behaviour.
Coroutines can be called simply by yielding. Yielded generators are adapted into GeneratorCoroutine instances so that they may be executed by the kernel. Coroutines are executed on the current strand, and as such execution of the caller is only resumed once the yielded coroutine has completed.
function hello()
{
echo 'Hello, ';
yield Recoil::noop();
}
function world()
{
echo 'world!' . PHP_EOL;
yield Recoil::noop();
}
Recoil::run(
function () {
yield hello();
yield world();
}
);
To return a value from a coroutine, simply use the return
keyword as you would in a normal function.
function multiply($a, $b)
{
yield Recoil::noop();
return $a * $b;
echo 'This code is never reached.';
}
Recoil::run(
function () {
$result = (yield multiply(3, 7));
echo '3 * 7 is ' . $result . PHP_EOL;
}
);
Because the return
keyword can not be used to return a value inside a generator before PHP version 7, the kernel API provides
Recoil::return_()
to send a value to the calling coroutine. Just like return
, execution of the coroutine stops when a
value is returned.
function multiply($a, $b)
{
yield Recoil::return_($a * $b);
echo 'This code is never reached.';
}
One of the major advantages made available by coroutines is that errors can be reported using familiar exception
handling techniques. Unlike return
, the throw
keyword can be used in the standard way inside PHP generators.
function multiply($a, $b)
{
if (!is_numeric($a) || !is_numeric($b)) {
throw new InvalidArgumentException;
}
yield Recoil::return_($a * $b);
}
Recoil::run(
function() {
try {
yield multiply(1, 'foo');
} catch (InvalidArgumentException $e) {
echo 'Invalid argument!';
}
}
);
Recoil::throw_()
is equivalent to a throw
statement, except that the presence of yield
forces PHP to parse the
function as a generator.
function onlyThrow()
{
yield Recoil::throw_(new Exception('Not implemented!'));
}
Recoil includes several features to allow interoperability with React and conventional React applications.
React streams can be adapted into Recoil streams using ReadableReactStream and WritableReactStream.
React promises can be yielded directly from a coroutine. The promise is adapted into a PromiseCoroutine instance and the calling coroutine is resumed once the promise has been fulfilled.
If the promise is resolved, the resulting value is returned from the yield statement. If it is rejected, the yield statement throws an exception describing the error. If a strand is waiting on the resolution of a cancellable promise, and execution of that strand is terminated the promise is cancelled. Recoil does not yet support progress events.
The promise-dns example demonstrates using the React DNS component, a promised-based API, to resolve several domain names concurrently. This example shows the same functionality implemented without Recoil.
In all of the examples above, the Recoil::run()
convenience function is used to start the kernel. Internally this
function chooses an appropriate event-loop implementation, instantiates the kernel, enqueues the given function for
execution and runs the event-loop.
An existing event-loop can be used by passing it as the second parameter.
$eventLoop = new React\EventLoop\StreamSelectLoop;
Recoil::run(
function () {
echo 'Hello, world!' . PHP_EOL;
yield Recoil::noop();
},
$eventEventLoop
);
Note that the event-loop will be started by Recoil::run()
, hence this function will block until there are no more
pending events.
To attach a coroutine kernel to an existing event-loop without assuming ownership the kernel must be instantiated manually.
$eventLoop = new React\EventLoop\StreamSelectLoop;
$kernel = new Recoil\Kernel\Kernel($eventLoop);
$coroutine = function () {
echo 'Hello, world!' . PHP_EOL;
yield Recoil::noop();
};
$kernel->execute($coroutine());
$eventLoop->run();
- Follow @IcecaveStudios on Twitter
- Visit the Icecave Studios website
- Join
#icecave
on irc.freenode.net