swactor/docs/runtime/actor-model.md
Developer 59845f4059 feat: Docker realization, node binary, docs reorg, and simulation testing
Docker realization (bridging simulation to real TCP):
- NodeDriver (`crates/distribution/src/driver.rs`): bridges DistributedNode
  tick loop to TcpTransport with piggyback-extended wire messages
- swactor-node binary (`crates/node/`): CLI node with --listen, --seed,
  --dashboard-port, --actors flags
- Dockerfile: multi-stage build (rust:1.93-slim → debian:bookworm-slim)
- Docker integration tests (`tests/docker/`): 5-node cluster with 4
  scenarios (convergence, failure detection, actor resolution, rejoin)
- LAN cluster scripts for cross-machine validation
- TCP transport retry-on-stale-connection logic
- /api/distribution REST endpoint on dashboard (feature-gated)
- Piggyback fields (piggyback + from_addr) on Ping/Ack/PingReq messages

Docs reorganization:
- docs/runtime/ — actor-model, runtime, worker-thread, channels
- docs/distribution/ — distribution, swim, kademlia, transport
- docs/diagrams/ — all SVG files
- docs/connectome/ — connectome analysis
- docs/development_history/ — DOCKER_REALIZATION.md, SIMULATION_TESTING.md
- render_docs.sh outputs to docs/diagrams/
- README links updated to new paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 07:45:44 +00:00

6.7 KiB

Actor Model

Swactor's actor model is intentionally minimal. An actor is a struct that implements one trait, receives one message type, and communicates only through Ctx.

Defining an Actor

use swactor::actor::ActorInterface;
use swactor::runtime::Ctx;

#[derive(Debug, Default, Clone)]
struct Ping { return_addr: ActorAddress }

#[derive(Debug, Default, Clone)]
struct Pong;

struct MyActor {
    count: usize,
}

impl ActorInterface for MyActor {
    type Incoming = Ping;
    type Response = Pong;     // not enforced at runtime — a documentation hint

    fn handle(&mut self, ctx: &Ctx, msg: Ping) {
        self.count += 1;
        let _ = ctx.send(msg.return_addr, Pong);
    }
}

That's it. No lifecycle hooks, no supervision trees, no async. Just a handle method.

The Traits

┌─ Message ─────────────────────────────────────────────────────────────────┐
│                                                                           │
│  trait Message: 'static + Sized + Clone + Send + Sync {}                  │
│                                                                           │
│  Blanket-implemented for any type that meets the bounds.                  │
│  You never implement this manually.                                       │
│                                                                           │
│  Why Clone + Send + Sync?                                                 │
│    Clone  — messages may be duplicated (Python bindings, stats, etc.)     │
│    Send   — messages cross thread boundaries                              │
│    Sync   — required by the type-erased Any + Send path                   │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

┌─ ActorInterface ──────────────────────────────────────────────────────────┐
│                                                                           │
│  trait ActorInterface: 'static + Send {                                   │
│      type Incoming: Message;                                              │
│      type Response: Message;                                              │
│      fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming);                │
│  }                                                                        │
│                                                                           │
│  This is what you implement. The actor owns mutable state (&mut self)     │
│  and receives typed messages.                                             │
│                                                                           │
│  Actors are Send but NOT Sync — only one worker thread ever touches       │
│  a given actor.                                                           │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

Type Erasure

Actors in the runtime are stored as Box<dyn AnyActor>, which erases the concrete type. Messages are stored as Box<dyn Any + Send>. Type checking happens at delivery time via downcast:

                        compile time                    runtime
                        ───────────                    ───────
  ctx.send(addr, msg)
       │
       v
  Box::new(msg) as Box<dyn Any + Send>     -- type erased here
       │
       v
  enqueued in mailbox (VecDeque<Box<dyn Any + Send>>)
       │
       v
  actor.handle_any(ctx, msg)
       │
       v
  msg.downcast::<A::Incoming>()            -- type recovered here
       │
  ┌────┴────┐
  │         │
  ok        err
  │         │
  v         v
  A.handle  silently dropped
  (ctx,msg)

Why silent drop? In a dynamic system (especially with Python bindings), type mismatches aren't crashes — they're routing errors. The actor simply ignores messages it doesn't understand.

ActorAddress

┌─ ActorAddress ────────────────────────────────────────────────────────────┐
│                                                                           │
│  pub struct ActorAddress(pub [u8; 32]);                                   │
│                                                                           │
│  32 random bytes — globally unique, no coordination needed.               │
│  Generated via get_random() (system RNG or deterministic counter          │
│  for WASM builds).                                                        │
│                                                                           │
│  Derives: Debug, Default, Clone, Copy, PartialEq, Eq, Hash               │
│                                                                           │
│  Used as keys in:                                                         │
│    AddressMap  (actor → worker lookup)                                    │
│    ActorPool   (actor → mailbox + state)                                  │
│    InboxRegistry (external inbox lookup)                                  │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

Where Things Live in the Code

Concept File Key lines
Message trait src/actor.rs blanket impl
ActorInterface trait src/actor.rs user-facing trait
ActorAddress src/actor.rs 32-byte random ID
Actor<A> wrapper src/actor.rs wraps user state
AnyActor trait src/actor.rs type-erased handler
ActorPool src/worker/mod.rs per-worker storage
ActorSlot src/worker/mod.rs mailbox + actor pair