:: 710 Words

More Actors

with Tokio

bg right:66%


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

sepia

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:

bg fit 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

  • enum is perfect for this
  • Type of actual actor is erased
  • Mocking is not required
type Handle = mpsc::Sender<Message>;

Message definition:

#[derive(...)]
pub enum Message {
    Plate(PlateRecord),
    WantHeartbeat(Duration),
    IAmCamera(Camera),
    IAmDispatcher(Vec<u16>),
}

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?

bg right:20% grayscale


bg 80%


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.