🎭 Actor Model
Erlang-style message passing concurrency with supervision trees
Overview
The Actor model is a concurrency paradigm where independent actors communicate exclusively through asynchronous message passing — no shared mutable state. Each actor has a private mailbox, processes messages sequentially, and can spawn child actors, creating supervision hierarchies for fault tolerance.
This implementation is inspired by Erlang/OTP and Akka, featuring typed mailboxes, supervision trees, behavior switching, routers, and a dead letter handler. It includes 38 built-in tests.
Concepts Demonstrated
- Message-passing concurrency — actors communicate without shared state
- Supervision trees — one-for-one, one-for-all, rest-for-one restart strategies
- Higher-order functions as behaviors — actor logic is a function
'msg → unit - Behavior switching —
become/unbecomefor state machines - Variant types — message envelopes, supervision strategies, actor lifecycle events
- Mutable state encapsulation — each actor's state is isolated in its context
- Pattern matching — selective receive dispatches messages by type
How It Works
(* Actor references for addressing *)
type actor_ref = {
id: actor_id;
name: string option;
}
(* Messages wrapped in envelopes *)
type 'msg envelope = {
from: actor_ref option;
to_ref: actor_ref;
payload: 'msg;
timestamp: float;
correlation_id: string option;
}
(* Supervision strategies *)
type restart_strategy =
| OneForOne (* restart only the failed child *)
| OneForAll (* restart all children *)
| RestForOne (* restart failed child + all children started after it *)
(* Define an actor's behavior *)
let counter_behavior ctx msg =
match msg with
| Increment -> ctx.state := !(ctx.state) + 1
| GetCount reply_to ->
send reply_to (Count !(ctx.state))
(* Spawn and communicate *)
let counter = spawn system "counter" counter_behavior
send counter Increment
send counter Increment
let count = ask counter (GetCount self) (* → Count 2 *)
Key Components
| Component | Description |
|---|---|
ActorSystem | Top-level container that manages all actors, message routing, and dead letters |
Actor | Unit of computation with a mailbox, behavior function, and optional parent |
Supervisor | Special actor that monitors children and applies a restart strategy on failure |
Router | Distributes messages across a pool of actors (round-robin or broadcast) |
Registry | Named actor lookup — find actors by name instead of reference |
DeadLetterHandler | Captures messages sent to stopped or non-existent actors |
Supervision Strategies
- One-for-One: Only the failed actor is restarted. Other siblings are unaffected. Use when children are independent.
- One-for-All: All children are stopped and restarted when any child fails. Use when children are interdependent.
- Rest-for-One: The failed child and all children started after it are restarted. Use when children have ordered dependencies.
Design Philosophy: "Let It Crash"
Rather than defensive error handling in every function, actors delegate failure recovery to supervisors. If an actor encounters an unexpected error, it crashes. Its supervisor detects the failure and decides whether to restart it, escalate, or stop. This produces cleaner application code and more robust systems.
Running
# Compile and run with tests
ocamlfind ocamlopt -package unix -linkpkg actor.ml -o actor && ./actor
Further Reading
- Erlang/OTP Design Principles
- Raft Consensus — distributed consensus, related distributed systems topic
- Source code