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