swactor/ARCHITECTURE.md
2026-02-06 13:48:51 +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: &Ctx, msg)
│                          #   - actors depend ONLY on Ctx, nothing else
│
├── context.rs             # Ctx wrapper — the "syscall interface" for actors
│                          #   - wraps &dyn ContextInner (solves object-safety)
│                          #   - 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
│
├── channel/
│   └── mod.rs             # HybridChannel — per-worker MPSC
│                          #   - the ONE concurrent data structure on the hot path
│                          #   - carries Envelope (cross-worker) and spawn tuples
│
├── 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 for ActorAddress → Actor lookup
│                          #   - insert on spawn, remove on shutdown
│
├── runtime.rs             # Runtime — the composition point
│                          #   - creates workers, address map
│                          #   - implements ContextInner (delegates to address map + transfer queues)
│                          #   - public API: new(), spawn(), send_to(), run(), tick(), shutdown()
│
├── config.rs              # RuntimeConfig — tuning knobs
│                          #   - num_threads, max_actors, actor_max_messages
│                          #   - mailbox_waterlevel (drain threshold per actor)
│                          #   - BackoffPolicy (spin/yield/sleep thresholds)
│
└── placement.rs           # Actor placement strategy
                           #   - currently: round-robin across workers

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 channel/mod.rs HybridChannel 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 Ctx wrapper over &dyn ContextInner. 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. Currently round-robin. 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   │
  └───────────────────────┘