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>
6.7 KiB
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 |