Tiny Tokio Actors

Ferdinand de Antoni
5 min readMay 9, 2021

--

There are a couple of actor libraries available for Rust, the most well known one being Actix. I found the library not that easy to use however, especially when using async/await. After getting to know Tokio a little better (the runtime which Actix also uses), I realised that the basic building blocks for creating an actor like framework are available in Tokio itself. In simplest terms, you can use Tokio’s channels to pass messages around to structs that then act like actors. You just need to define handlers on the struct that define the behaviour you want per message type.

So to structure my existing code a little better, I decided to create a tiny framework that makes it easier for me to implement an actor model on top of Tokio. From there the Tiny Tokio Actor crate was born.

To use it, simply add the following dependency to your project:

[dependencies]
tiny-tokio-actor = "0.2"

Next, import the crate and define a message you want to have the ActorSystem use in its Event Bus. Each ActorSystem has an event bus that all actors that run on it can send and receive events on. You can define a struct as a SystemEvent as follows:

(if you want to copy/paste the code, the complete gist is on github)

The struct must derive Clone. Using Debug is entirely optional. With this done, we can go and create the actor system on which we will run our actors:

To create the system, we first need to create it’s message bus. We create a message bus that will send/receive TestEvent messages and will have a buffer of 1000 messages.

With our actor system created, let’s define an actor struct that we will run on this system:

Implementing the pre_start() and post_stop() methods is entirely optional, the defaults do nothing. These methods are good if you want to initialise your actor in some way, and also control its “shutdown”.

Notice also that we need to decorate the impl block with #[async_trait] as we use async functions inside the trait. See the async-trait crate for more info about this. Note that this crate re-exports async-trait so there is no need to add it as a separate dependency.

After defining the Actor, next we define the messages we like to send to it:

The message struct must derive Clone, Debug is optional. We also define what response we want the Actor to send back when it receives this message. That is what the Response type is for. Here we define it as returning a String.

Now that we have our message, we can go ahead and define the behaviour our actor will have when it receives the message:

The behaviour is very simple: when we receive TestMessage we publish a message on the actor system’s event bus, increase the internal counter by 1, and send back a String containing “Ping!”.

If we want our actor to handle another message type, simply create that message type first, and then implement the Handler for that other type. For example:

Our OtherMessage is very similar to the pervious message, except that it holds a usize instead of String, and also send back a usize instead of String. We can see the behaviour first publish a message on the system’s event bus, and then set the actor’s counter to the value of the message, and then return the value of its counter.

Now that we have our actor and it’s behaviour defined, we can run it on our actor system. First though, let’s set up a loop that listen’s our actor system’s event bus:

Now launch the actor on the system:

When we create the actor, the actor system will return an ActorRef to it. This actor reference allows you to send messages to the actor. For example:

Here we ask the actor a message, which means we also want a response. We can also tell the actor a message, in which case the message sending becomes “fire-and-forget”, i.e. we do not wait for a response.

So there we have it, an actor system running one actor on top of Tokio. Here is the code all in one gist:

You can also check out the code in the project here.

Using this actor framework allows me to organise my Tokio code a little better, but all the usual caveats apply! Do not block the event loop, instead hand over compute intensive tasks to something like rayon. Also be mindful of deadlocks if your actor graph is not acyclic. Actors do not save you from this, especially if you have actors sending messages to each other in a loop without back-pressure. Since this crate uses Tokio’s unbounded channel for an actor’s mailbox, if one actor is a little slow while other actors that send it messages are fast, the slow actor’s mailbox may fill up and grind your system to a halt.

So this little crate can help with structuring your code, but it will not save you from the usual concurrency problems that come with unbalanced producers/consumers. To read up more about this, I highly suggest reading Actors with Tokio on rhyl.io, including its accompanying reddit post.

--

--