Table of Contents
More Actors
with Tokio
ryhl.io/blog/actors-with-tokio (2021)
barafael.github.io/posts/more-actors-with-tokio (2025)
Actor: (my) Street Definition
- Autonomous unit isolates state/process
- Communicates with others via message passing (channels)
- Channel kinds outline system topology
Inspired by Alan Kay on Quora, the real one had similar ideas.
Architecture
- Birds-eye-view: complex but manageable
- Only actors and channels!
- Fun and useful to follow messages along channels.
Protohackers Exercise 6 :arrow_right:
Architecture is concerned with distributing responsibility
An Actor should be its data
Adapted from Alices original example:
#[derive(Debug, PartialEq, Eq)]
pub struct UniqueIdService {
next_id: u32,
}
No runtime resources (sockets, channel handles, etc.) here! They should belong to the actor event loop future.
Responsibility is Ownership
The Core Idea is messaging
|
Message definition:
|
Channels are a tool for transferring ownership
Graceful shutdown
An actor should shut down when its primary means of communication goes away:
- Socket closes
- Channel becomes empty and there are no more senders
- Timeout occurs
When an actor exits, it drops its handles toward other actors - signaling them to exit, too.
Is Rust OOP or not?
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.
You could be dumb enough to use these ideas to simulate older, more fragile, less scalable ideas — like “procedures and data” — but who would be so bound to the past to make that enormous blunder?
— Alan Kay
Maybe Rust + actors is OOOP (original object oriented programming).
Questions?
Aside: Deterministic Unit Tests
Playbook: Construct Data :arrow_right: Enqueue Messages :arrow_right: drop sender :arrow_right: Run event loop to completion :arrow_right: assert on results
- Avoid spawning.
- Avoid defining actor mocks - channels suffice.
- Avoid at all cost having to wait 1s to reach a state :shaking_face:.
Use the type system to inject resources (Stream
, AsyncRead
, etc.).
System becomes protocol-agnostic.
The Event Loop
async fn
which consumes self
and runtime resources.
impl UniqueIdService {
pub async fn event_loop(mut self, mut rx: mpsc::Receiver<Message>) -> Self {
loop {
select! {
...
}
}
}
Loop-select is a real superpower.
event_loop
returns Self
considered :ok_hand:
Read the blog post for details :shrug:
Advantages:
- Tests: can assert on the guts.
- Shutdown: can act on leftovers.
- Restart: can inject in fresh instance.
- Distributed actors: can move data elsewhere and restart.