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:
- are simple
class/struct
that inherit fromultramarine::actor
. - need some implementation details hidden behind the macro
ULTRAMARINE_DEFINE_ACTOR
- have a unique
key
. By default the type of actor’s key is a long unsigned int (seeKeyType
in ultramarine::actor)
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.