# 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` 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 │ └───────────────────────┘ ```