swactor/ARCHITECTURE.md
Zachery Aaron Shores-Chmielewski 172e78a50b feat: mailbox
In the middle of a vibe-coded infra change. Modularizing the components into APIs amenable to lots of testing and optimization.
2026-02-05 21:22:36 +07:00

13 KiB

Swactor Architecture

System Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Runtime                                        │
│                      (composes everything)                                  │
│                                                                             │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                         Address Map                                   │  │
│  │                   ActorAddress → WorkerId                             │  │
│  │              (shared across all workers, read-heavy)                  │  │
│  └──────┬──────────────────┬──────────────────────┬─────────────────────┘  │
│         │                  │                      │                         │
│         ▼                  ▼                      ▼                         │
│  ┌─────────────┐    ┌─────────────┐       ┌─────────────┐                  │
│  │  Worker 0   │    │  Worker 1   │  ...  │  Worker N   │                  │
│  │  (thread)   │    │  (thread)   │       │  (thread)   │                  │
│  │             │    │             │       │             │                  │
│  │ ┌─────────┐ │    │ ┌─────────┐ │       │ ┌─────────┐ │                  │
│  │ │ Actor A │ │    │ │ Actor C │ │       │ │ Actor E │ │                  │
│  │ │ [═════] │ │    │ │ [═════] │ │       │ │ [═════] │ │                  │
│  │ │ mailbox │ │    │ │ mailbox │ │       │ │ mailbox │ │                  │
│  │ └─────────┘ │    │ └─────────┘ │       │ └─────────┘ │                  │
│  │ ┌─────────┐ │    │ ┌─────────┐ │       │ ┌─────────┐ │                  │
│  │ │ Actor B │ │    │ │ Actor D │ │       │ │ Actor F │ │                  │
│  │ │ [═════] │ │    │ │ [═════] │ │       │ │ [═════] │ │                  │
│  │ │ mailbox │ │    │ │ mailbox │ │       │ │ mailbox │ │                  │
│  │ └─────────┘ │    │ └─────────┘ │       │ └─────────┘ │                  │
│  │             │    │             │       │             │                  │
│  │ ┌─────────┐ │    │ ┌─────────┐ │       │ ┌─────────┐ │                  │
│  │ │Transfer │◄├────├─┤Transfer │◄├───────├─┤Transfer │ │                  │
│  │ │ Queue   │ │    │ │ Queue   │ │       │ │ Queue   │ │                  │
│  │ │ (MPSC)  │─├────├►│ (MPSC)  │─├───────├►│ (MPSC)  │ │                  │
│  │ └─────────┘ │    │ └─────────┘ │       │ └─────────┘ │                  │
│  └─────────────┘    └─────────────┘       └─────────────┘                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘


                         ══ = VecDeque (no atomics)

Message Flow

 SAME WORKER (fast path — zero atomics)
 ═══════════════════════════════════════

   Actor A                        Actor B
   handle() {                     mailbox (VecDeque)
     ctx.send(addr_B, msg)          ▲
       │                            │
       ├─ address_map[addr_B]       │
       │  → Worker 0 (that's me!)   │
       │                            │
       └─ mailbox_B.push(msg) ──────┘
   }                              no atomics, no envelope


 CROSS-WORKER (one atomic hop)
 ═══════════════════════════════

   Actor A (Worker 0)             Worker 1              Actor C (Worker 1)
   handle() {                     transfer queue         mailbox (VecDeque)
     ctx.send(addr_C, msg)            ▲                      ▲
       │                              │                      │
       ├─ address_map[addr_C]         │                      │
       │  → Worker 1 (not me)         │                      │
       │                              │                      │
       └─ envelope(addr_C, msg) ──────┘                      │
           (atomic push)              │                      │
                                      └── worker 1 pops ─────┘
                                          and distributes
                                          (local, no atomic)
   }

Worker Loop

  ┌─────────────────────────────────────────────┐
  │              Worker Thread                    │
  │                                               │
  │  loop {                                       │
  │    ┌──────────────────────────────────────┐   │
  │    │ 1. DRAIN TRANSFER QUEUE              │   │
  │    │    while let Some((addr, env)) =     │   │
  │    │          transfer_queue.pop()        │   │
  │    │    {                                 │   │
  │    │      local_actors[addr].mailbox      │   │
  │    │          .push(env.unpack())         │   │
  │    │    }                                 │   │
  │    └──────────────────────────────────────┘   │
  │    ┌──────────────────────────────────────┐   │
  │    │ 2. TICK ACTORS                       │   │
  │    │    for actor in &mut actor_pool {    │   │
  │    │      let n = drain_count(actor);     │   │
  │    │      for _ in 0..n {                 │   │
  │    │        let msg = actor.mailbox.pop();│   │
  │    │        actor.handle(&ctx, msg);      │   │
  │    │      }                               │   │
  │    │    }                                 │   │
  │    └──────────────────────────────────────┘   │
  │    ┌──────────────────────────────────────┐   │
  │    │ 3. IDLE?                             │   │
  │    │    if no messages processed:         │   │
  │    │      spin → yield → park             │   │
  │    └──────────────────────────────────────┘   │
  │  }                                            │
  └───────────────────────────────────────────────┘

File Tree

src/
├── lib.rs                 # crate root, feature flags, public exports
├── error.rs               # Error type
│
├── actor.rs               # Message trait, ActorInterface trait, ActorAddress
│                          #   - ActorInterface::handle(&mut self, ctx: &dyn Context, msg)
│                          #   - actors depend ONLY on Context, nothing else
│
├── context.rs             # Context trait — the "syscall interface" for actors
│                          #   - send(), self_addr(), spawn()
│                          #   - this is ALL actors can see of the framework
│
├── envelope.rs            # Envelope type — type erasure for cross-thread messages
│                          #   - wraps typed messages for the transfer queue
│                          #   - unwraps back to concrete type at destination
│
├── address_map.rs         # ActorAddress → WorkerId mapping
│                          #   - shared read-heavy structure
│                          #   - written on spawn, read on every send
│
├── transfer.rs            # Transfer queue — per-worker MPSC
│                          #   - the ONE concurrent data structure on the hot path
│                          #   - carries (ActorAddress, Envelope) pairs
│
├── worker/
│   ├── mod.rs             # Worker struct and worker loop
│   │                      #   - owns actor pool + transfer queue
│   │                      #   - the thread boundary: concurrent outside, local inside
│   │                      #   - drain transfer queue → tick actors → backoff
│   │
│   ├── mailbox.rs         # VecDeque-based local mailbox
│   │                      #   - NO atomics, NO Arc, NO crossbeam
│   │                      #   - only touched by the owning worker thread
│   │
│   └── pool.rs            # Actor pool — stores actors assigned to this worker
│                          #   - local HashMap or Vec for ActorAddress → Actor lookup
│                          #   - insert on spawn, remove on shutdown
│
├── runtime.rs             # Runtime — the composition point
│                          #   - creates workers, address map
│                          #   - implements Context (delegates to address map + transfer queues)
│                          #   - public API: new(), spawn(), send_to(), run(), tick(), shutdown()
│
├── config.rs              # RuntimeConfig — tuning knobs
│                          #   - num_threads, max_actors, mailbox capacity
│                          #   - drain strategy, backoff policy
│                          #   - placement strategy (round-robin, caller-affinity, etc.)
│
└── placement.rs           # Actor placement strategy
                           #   - decides which worker a new actor goes to
                           #   - round-robin, least-loaded, caller-affinity

Components

Component File(s) What It Does Concurrent?
Worker worker/mod.rs Owns a thread, a pool of actors, their mailboxes, and a transfer queue. Runs the tick loop. Everything inside is single-threaded. No (that's the point)
Mailbox worker/mailbox.rs VecDeque<M> per actor. Zero atomics. Only the owning worker reads/writes. No
Actor Pool worker/pool.rs Stores actors on this worker. Local lookup by address. No
Transfer Queue transfer.rs MPSC queue per worker. The only atomic boundary. Other workers push, this worker pops. Yes (the ONE place)
Address Map address_map.rs Maps ActorAddress → WorkerId. Read on every cross-thread send, written on spawn. Yes (read-heavy)
Envelope envelope.rs Type-erases messages for the transfer queue. Unwrapped at destination. No (data format)
Context context.rs Trait that actors see. send(), self_addr(), spawn(). Hides all framework internals. N/A (trait)
Runtime runtime.rs Wires it all together. Creates workers, holds address map, exposes public API. Minimal (delegates)
Placement placement.rs Decides which worker gets a new actor. No (called at spawn time)

Single-Threaded / WASM Mode

One worker. No transfer queue needed. No address map needed (everything is local). The system collapses to:

  Worker 0
  ┌───────────────────────┐
  │  Actor A  [mailbox]   │
  │  Actor B  [mailbox]   │     All sends are local.
  │  Actor C  [mailbox]   │     All mailboxes are VecDeque.
  │                       │     Zero atomics anywhere.
  │  tick() drives loop   │
  └───────────────────────┘