diff --git a/content/code/thinking-about-rust-actors/actors.png b/content/code/thinking-about-rust-actors/actors.png new file mode 100644 index 0000000..bb75425 Binary files /dev/null and b/content/code/thinking-about-rust-actors/actors.png differ diff --git a/content/code/thinking-about-rust-actors/index.md b/content/code/thinking-about-rust-actors/index.md new file mode 100644 index 0000000..fe63722 --- /dev/null +++ b/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. + + + +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) { + let (tx, mut rx) = mpsc::channel::(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; +} +``` + +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; +} +```