199 lines
13 KiB
Markdown
199 lines
13 KiB
Markdown
# 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 │
|
|
└───────────────────────┘
|
|
```
|