Developing a simple actor application

As an example, let’s create a simple program computing different Fibonacci numbers.

In this example, we will use the naive divide-and-conquer approach of spawning actors, each responsible for computing a single index number. They then spawn other actors, as necessary. We can visualize the computation as such:

Defining an actor

Here is an empty fibonacci_actor actor definition:

class fibonacci_actor : public ultramarine::actor<fibonacci_actor> {
public:
    ULTRAMARINE_DEFINE_ACTOR(fibonacci_actor, (fib));
    seastar::future<int> fib(); // compute Fibonacci number for index `this->key`
};

Notice that in Ultramarine, actors:

Using actors

Now, we can call our actor from anywhere in our Seastar code. To call an actor, we first need to create a reference to it:

auto ref = ultramarine::get<fibonacci_actor>(24);
return ref->fib().then([] (int value) {
    seastar::print("Result: %d\n", value);
});

An actor reference is a trivial object that you can use to refer to an actor. They are forgeable, copiable and movable. Because Ultramarine is a Virtual Actor framework, you do not need to create any actor. They are created as needed.

Message handler implementation

Now that the interface and calling site for our actor are set-up, we can implement fibonacci_actor::fib:

seastar::future<int> fibonacci_actor::fib() {
    if (key <= 2) {
        return seastar::make_ready_future<int>(1);
    } else {
        auto f1 = ultramarine::get<fibonacci_actor>(key - 1)->fib();
        auto f2 = ultramarine::get<fibonacci_actor>(key - 2)->fib();
        return seastar::when_all_succeed(std::move(f1), std::move(f2)).then([] (auto r1, auto r2) {
            return r1 + r2;
        });
    }
}

That’s it! To compute our Fibonacci number, we divide the problem in two and delegate to two other actors. Once they are done, we combine the result and return. Better yet, this code is natively multi-threaded, because all actors are evenly spread throughout all available hardware.