Skip to content

Latest commit

 

History

History
323 lines (237 loc) · 11.7 KB

README.md

File metadata and controls

323 lines (237 loc) · 11.7 KB

Recoil

Build Status Test Coverage SemVer

Recoil is a generator-based cooperative multitasking kernel for React.

Overview

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.

Concepts

Coroutines

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

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 and Kernel API

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

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

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.

Examples

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.

Basic execution

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.

Calling one coroutine from another

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();
    }
);

Returning a value from a coroutine

PHP 7

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;
    }
);

PHP 5

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.';
}

Throwing and catching exceptions

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!'));
}

Cooperating with React

Recoil includes several features to allow interoperability with React and conventional React applications.

Streams

React streams can be adapted into Recoil streams using ReadableReactStream and WritableReactStream.

Promises

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.

Using an existing event-loop

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.

Instantiating the kernel manually

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();

Contact us