Browse Source

Thinking about Actors in Rust

master
Julio Biason 9 months ago
parent
commit
9dff9ae98a
  1. BIN
      content/code/thinking-about-rust-actors/actors.png
  2. 97
      content/code/thinking-about-rust-actors/index.md

BIN
content/code/thinking-about-rust-actors/actors.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

97
content/code/thinking-about-rust-actors/index.md

@ -0,0 +1,97 @@
+++
title = "Thinking About Rust Actors"
date = 2023-08-11
draft =true
[taxonomies]
tags = ["rust", "actor model"]
+++
I recently wrote an application for work (so, sorry, can't show you the code)
that, 'cause it was heavily I/O based, I decided to write it using
[Tokio](https://tokio.rs/) and the idea of [Actor Model with
it](https://ryhl.io/blog/actors-with-tokio/).
... which gave me some things to think about.
<!-- more -->
Before anything, actors in Rust are very different from the actors in languages
with the actual Actor Model. In summary, you have your actors, which running
independently, each actor have an Inbox for things to be processed and an
"outbox" -- in quotes, 'cause that's not really it. An actor can receive a
message, process it and then it can just be done with it or it can produce
something that it is send to another actor -- that's its outbox, which usually
differs from the Inbox 'cause the Inbox need to have a queue of sorts, but the
Outbox doesn't (and that's why I've been using "outbox" with quotes before).
All the messages are delivered by a "post office" of sorts, that connects all
Actors:
![](actors.png "A silly representation of the actor model")
On my implementation, the actor is actually a module with a `run()` function;
this function exposes the `Sender` part of a channel which acts as the Inbox of
it and the task PID, so the can `.await` it to avoid the main application from
finishing with the actor still running.
And because there is no "Post Office" kind of solver in Rust, we can actually
short circuit the actors by giving the `Sender` channel of an actor as
parameter to a second, so it knows where to send its messages. Something like:
```rust
let channel3 = actor3::run(...).await;
let channel2 = actor2::run(channel3).await;
actor1::run(channel2).await;
```
In this short sample, whatever "actor1" produces, it sends directly to
"actor2"; "actor2", on its part, produces something that is received by
"actor3". And, with more actors, things just keep chaining.
{% note() %}
I am intentionally ignoring the internals of each actor and their `run()`
function, but they are some variations of:
```rust
fn run(..) -> (task::JoinHandle<()>, mpsc::Sender<TheKindOfMessageTheActorAccepts>) {
let (tx, mut rx) = mpsc::channel::<TheKindOfMessageTheActorAccepts>(SOME_SIZE);
let task = tokio::spawn(async move {
while let Some(incoming) = rx.recv().await {
let conversion = actor_process(incoming);
// maybe send the conversion to the next actor?
}
});
(task, tx)
}
```
{% end %}
But... 'cause the actors have (very similar) interfaces, that looks like a
trait!
So, what should be the Actor trait?
First thing, its `new()` or similar function should expose its PID. Something
like:
```rust
pub trait Actor {
fn new(..) -> Sender<TheKindOfMessageTheActorAccepts>;
}
```
Why `TheKindOfMessageTheActorAccepts`? That's because each actor may have a
different input message. If we take our short sample above, "actor2" may be
receiving `usize`s and sending them as `String`s to "actor3".
Because that type may change from actor to actor, it should be an associated
type:
```rust
pub trait Actor {
type Input = TheKindOfMessageTheActorAccepts;
fn new(..) -> Sender<Self::Input>;
}
```
Loading…
Cancel
Save