Julio Biason
1 year ago
2 changed files with 97 additions and 0 deletions
After Width: | Height: | Size: 43 KiB |
@ -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…
Reference in new issue