docs: update runtime docs to reflect architecture changes from Cycles 1-3

Worker struct: TimerWheel → generic WorkerExtension. TickContext: registries
moved behind RuntimeExtension. Phases 2.5/5.5/7 generalized. Ctx API split
into core (6 methods) vs extension traits (swactor-std).

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 10:13:10 +00:00
parent 33df021afd
commit 803289c7dc
2 changed files with 71 additions and 66 deletions

View file

@ -16,11 +16,11 @@ messages.
│ │ │ │ │ │ │ │
│ │ address_map: Arc<AddressMap> -- actor -> worker lookup │ │ │ │ address_map: Arc<AddressMap> -- actor -> worker lookup │ │
│ │ inbox_registry: Arc<InboxRegistry> -- external inbox delivery │ │ │ │ inbox_registry: Arc<InboxRegistry> -- external inbox delivery │ │
│ │ name_registry: Arc<NameRegistry> -- name -> address lookup │ │
│ │ monitor_registry: Arc<MonitorRegistry> -- death watch subscripts │ │
│ │ group_registry: Arc<GroupRegistry> -- pub-sub actor groups │ │
│ │ placement: Placement -- load-aware worker picker │ │ │ │ placement: Placement -- load-aware worker picker │ │
│ │ worker_stats: Vec<Arc<WorkerStats>> -- atomic stat counters │ │ │ │ worker_stats: Vec<Arc<WorkerStats>> -- atomic stat counters │ │
│ │ extension: Option<Arc<dyn RuntimeExtension>> │ │
│ │ (StdExtension holds: NameRegistry, MonitorRegistry, │ │
│ │ GroupRegistry, WatchRegistry) │ │
│ │ │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
@ -75,35 +75,42 @@ only way for actors to interact with the outside world.
│ inner: &dyn ContextInner -- polymorphic dispatch │ │ inner: &dyn ContextInner -- polymorphic dispatch │
│ self_addr: ActorAddress -- address of the current actor │ │ self_addr: ActorAddress -- address of the current actor │
│ │ │ │
│ ┌─ Public API ────────────────────────────────────────────────────────┐ │ │ ┌─ Core API ─────────────────────────────────────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ ctx.self_addr() -> ActorAddress │ │ │ │ ctx.self_addr() -> ActorAddress │ │
│ │ ctx.send(addr, msg) -> Result<(), Error> │ │ │ │ ctx.send(addr, msg) -> Result<(), Error> │ │
│ │ ctx.spawn(actor) -> Result<ActorAddress, Error> │ │ │ │ ctx.spawn(actor) -> Result<ActorAddress, Error> │ │
│ │ ctx.spawn_named(name, actor) -> Result<ActorAddress, Error> │ │
│ │ ctx.spawn_restartable(a, f, max) -> Result<ActorAddress, Error> │ │
│ │ ctx.stop_self() │ │ │ │ ctx.stop_self() │ │
│ │ ctx.stop_actor(addr) -> Result<(), Error> │ │ │ │ ctx.stop_actor(addr) -> Result<(), Error> │ │
│ │ ctx.where_is(name) -> Option<ActorAddress> │ │ │ │ ctx.extension() -> Option<&dyn RuntimeExtension> │ │
│ │ ctx.monitor(target) -> MonitorRef │ │ │ │ │ │
│ │ ctx.demonitor(mref) │ │ │ └─────────────────────────────────────────────────────────────────────┘ │
│ │ ctx.join_group(group) │ │ │ │
│ │ ctx.leave_group(group) │ │ │ ┌─ Extension Traits (swactor-std) ──────────────────────────────────┐ │
│ │ ctx.publish(group, msg) -> usize │ │ │ │ │ │
│ │ ctx.group_members(group) -> Vec<ActorAddress> │ │ │ │ CtxNaming: spawn_named, where_is │ │
│ │ ctx.send_after_ticks(addr, msg, n) │ │ │ │ CtxMonitoring: monitor, demonitor │ │
│ │ ctx.send_interval_ticks(addr, msg, period) │ │ │ │ CtxWatching: watch, unwatch │ │
│ │ CtxGroups: join_group, leave_group, publish, group_members │ │
│ │ CtxTimers: send_after_ticks, send_interval_ticks │ │
│ │ │ │
│ │ These use ctx.extension() + downcast to StdExtension. │ │
│ │ Also: spawn_restartable (via CtxNaming) │ │
│ │ │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌─ ContextInner dispatch ─────────────────────────────────────────────┐ │ │ ┌─ ContextInner dispatch ─────────────────────────────────────────────┐ │
│ │ │ │ │ │ │ │
│ │ Five methods: send_any, spawn_any, request_stop, │ │
│ │ post_worker_request, extension │ │
│ │ │ │
│ │ In single-threaded mode: inner = &Runtime │ │ │ │ In single-threaded mode: inner = &Runtime │ │
│ │ send → transfer_txs[wid], spawn → spawn_txs[wid] │ │ │ │ send → transfer_txs[wid], spawn → spawn_txs[wid] │ │
│ │ │ │ │ │ │ │
│ │ In multi-threaded mode: inner = &WorkerContext │ │ │ │ In multi-threaded mode: inner = &WorkerContext │ │
│ │ send → pending_local (same worker) or transfer_txs (cross) │ │ │ │ send → pending_local (same worker) or transfer_txs (cross) │ │
│ │ spawn → spawn_txs[target_wid] │ │ │ │ spawn → spawn_txs[target_wid] │ │
│ │ post_worker_request → worker_requests (drained phase 5.5) │ │
│ │ │ │ │ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
@ -287,15 +294,22 @@ Actors can stop other actors from handlers:
ctx.stop_actor(other_addr)?; // PoisonPill semantics — queued after existing msgs ctx.stop_actor(other_addr)?; // PoisonPill semantics — queued after existing msgs
``` ```
## Per-Worker Timers ## Per-Worker Timers (swactor-std)
Deterministic tick-counting timers (not wall-clock): Deterministic tick-counting timers (not wall-clock). Requires `StdExtension`
and the `CtxTimers` extension trait:
``` ```
use swactor_std::CtxTimers;
ctx.send_after_ticks(addr, msg, 5); // one-shot: fires after 5 ticks ctx.send_after_ticks(addr, msg, 5); // one-shot: fires after 5 ticks
ctx.send_interval_ticks(addr, msg, 10); // repeating: every 10 ticks ctx.send_interval_ticks(addr, msg, 10); // repeating: every 10 ticks
``` ```
The `TimerWheel` lives as a per-worker extension (`WorkerExtension`),
created by `StdExtension::create_worker_extension()`. Timer requests are
dispatched via `ctx.post_worker_request()` and processed in phase 5.5.
## RuntimeHandle ## RuntimeHandle
Returned by `run()`. Holds `Arc<Runtime>` and the thread `JoinHandle`s. Returned by `run()`. Holds `Arc<Runtime>` and the thread `JoinHandle`s.

View file

@ -48,10 +48,12 @@
│ │ │ │ │ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │ │ └──────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌─ TimerWheel ────────────────────────────────────────────────────┐ │ │ ┌─ worker_ext: Option<Box<dyn WorkerExtension>> ─────────────────┐ │
│ │ current_tick: u64 │ │ │ │ Per-worker extension state, created by RuntimeExtension │ │
│ │ once_timers: Vec<OnceTimer> -- fire_at, dest, msg │ │ │ │ factory. StdExtension provides a TimerWheel here. │ │
│ │ interval_timers: Vec<IntervalTimer> -- period, dest, clone_msg │ │ │ │ on_tick() → fire due messages (phase 2.5) │ │
│ │ handle_request() → schedule timers etc. (phase 5.5) │ │
│ │ gc_dead() → clean up dead actor state (phase 7) │ │
│ └──────────────────────────────────────────────────────────────────┘ │ │ └──────────────────────────────────────────────────────────────────┘ │
│ │ │ │
└────────────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────────────┘
@ -69,11 +71,9 @@ Lives on `Arc<Runtime>`, shared read-only across all worker threads.
│ spawn_txs: &[Sender] -- one Sender per worker │ │ spawn_txs: &[Sender] -- one Sender per worker │
│ placement: &Placement -- load-aware worker picker │ │ placement: &Placement -- load-aware worker picker │
│ inbox_registry: &InboxRegistry -- external Inbox<M> receivers │ │ inbox_registry: &InboxRegistry -- external Inbox<M> receivers │
│ name_registry: &NameRegistry -- String -> ActorAddress │
│ monitor_registry: &MonitorRegistry -- death watch subscriptions │
│ group_registry: &GroupRegistry -- pub-sub actor groups │
│ config: &RuntimeConfig -- budget, backoff, etc. │ │ config: &RuntimeConfig -- budget, backoff, etc. │
│ stats_hook: Option<&dyn Hook> -- per-tick stats callback │ │ extension: Option<&dyn RuntimeExtension> -- shared ext │
│ stats_hook: Option<&dyn StatsHook> -- per-tick stats callback │
│ worker_threads: &[OnceLock<Thread>] -- for unpark on send/spawn │ │ worker_threads: &[OnceLock<Thread>] -- for unpark on send/spawn │
│ │ │ │
└────────────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────────────┘
@ -147,19 +147,16 @@ Lives on `Arc<Runtime>`, shared read-only across all worker threads.
│ └────────────────────────────────────────────────────────────────────┘│ │ └────────────────────────────────────────────────────────────────────┘│
│ │ │ │ │ │
│ v │ │ v │
│ PHASE 2.5 --- Fire Due Timers │ │ PHASE 2.5 --- Fire Per-Worker Extension │
│ ┌────────────────────────────────────────────────────────────────────┐│ │ ┌────────────────────────────────────────────────────────────────────┐│
│ │ ││ │ │ ││
│ │ timers.fire() (advances tick counter, collects due messages) ││ │ │ worker_ext.on_tick() → Vec<(dest, msg)> ││
│ │ (StdExtension provides TimerWheel: advances tick, fires due) ││
│ │ │ ││ │ │ │ ││
│ │ v ││ │ │ v ││
│ │ for (dest, msg) in timer_msgs: ││ │ │ for (dest, msg) in ext_msgs: ││
│ │ ┌──────────────┬──────────────┬─────────────────┐ ││ │ │ route_to_pool_or_remote(pool, tc, dest, msg) ││
│ │ │ local actor │ other worker │ inbox/unknown │ ││ │ │ local → pool.deliver | cross → transfer_tx | → inbox_registry ││
│ │ │ │ │ │ ││
│ │ │ pool.deliver │ transfer_tx │ inbox_registry │ ││
│ │ │ │ + unpark │ .try_deliver() │ ││
│ │ └──────────────┴──────────────┴─────────────────┘ ││
│ │ ││ │ │ ││
│ └────────────────────────────────────────────────────────────────────┘│ │ └────────────────────────────────────────────────────────────────────┘│
│ │ │ │ │ │
@ -171,7 +168,7 @@ Lives on `Arc<Runtime>`, shared read-only across all worker threads.
│ │ │ implements ContextInner │ ││ │ │ │ implements ContextInner │ ││
│ │ │ pending_local: RefCell<Vec<(Addr, Box<Any>)>> │ ││ │ │ │ pending_local: RefCell<Vec<(Addr, Box<Any>)>> │ ││
│ │ │ stop_requests: RefCell<Vec<ActorAddress>> │ ││ │ │ │ stop_requests: RefCell<Vec<ActorAddress>> │ ││
│ │ │ timer_requests: RefCell<Vec<TimerRequest>> │ ││ │ │ │ worker_requests: RefCell<Vec<Box<dyn Any + Send>>> │ ││
│ │ └────────────────────────────────────────────────────────────┘ ││ │ │ └────────────────────────────────────────────────────────────┘ ││
│ │ ││ │ │ ││
│ │ for each (addr, slot) in pool: ││ │ │ for each (addr, slot) in pool: ││
@ -215,10 +212,8 @@ Lives on `Arc<Runtime>`, shared read-only across all worker threads.
│ │ │ spawn_txs[wid].send((addr, actor)) + unpark │ ││ │ │ │ spawn_txs[wid].send((addr, actor)) + unpark │ ││
│ │ │ │ ││ │ │ │ │ ││
│ │ │ request_stop(addr): → stop_requests.push(addr) │ ││ │ │ │ request_stop(addr): → stop_requests.push(addr) │ ││
│ │ │ schedule_timer(req): → timer_requests.push(req) │ ││ │ │ │ post_worker_request(r): → worker_requests.push(r) │ ││
│ │ │ where_is(name): → name_registry.lookup(name) │ ││ │ │ │ extension(): → tc.extension │ ││
│ │ │ monitor(w, t): → monitor_registry.register(w, t) │ ││
│ │ │ join_group(a, g): → group_registry.join(g, a) │ ││
│ │ │ │ ││ │ │ │ │ ││
│ │ └────────────────────────────────────────────────────────────┘ ││ │ │ └────────────────────────────────────────────────────────────┘ ││
│ │ ││ │ │ ││
@ -242,12 +237,12 @@ Lives on `Arc<Runtime>`, shared read-only across all worker threads.
│ └────────────────────────────────────────────────────────────────────┘│ │ └────────────────────────────────────────────────────────────────────┘│
│ │ │ │ │ │
│ v │ │ v │
│ PHASE 5.5 --- Drain Timer Requests │ │ PHASE 5.5 --- Drain Worker Extension Requests │
│ ┌────────────────────────────────────────────────────────────────────┐│ │ ┌────────────────────────────────────────────────────────────────────┐│
│ │ ││ │ │ ││
│ │ for request in timer_requests: ││ │ │ for request in worker_requests: ││
│ │ Once { dest, msg, ticks } → timers.add_once(dest, msg, ticks) ││ │ │ worker_ext.handle_request(request) ││
│ │ Interval { dest, msg, p } → timers.add_interval(dest, msg, p) ││ │ │ (StdExtension: downcasts to TimerRequest, schedules timers) ││
│ │ ││ │ │ ││
│ └────────────────────────────────────────────────────────────────────┘│ │ └────────────────────────────────────────────────────────────────────┘│
│ │ │ │ │ │
@ -272,23 +267,20 @@ Lives on `Arc<Runtime>`, shared read-only across all worker threads.
│ │ stopping actors: call on_stop(&ctx) before removal ││ │ │ stopping actors: call on_stop(&ctx) before removal ││
│ │ poisoned actors: skip on_stop (state may be corrupt) ││ │ │ poisoned actors: skip on_stop (state may be corrupt) ││
│ │ ││ │ │ ││
│ │ for each dead (addr, reason): ││ │ │ for each dead addr: ││
│ │ address_map.remove(&addr) ││ │ │ address_map.remove(&addr) ││
│ │ name_registry.unregister_by_addr(&addr) ││ │ │ ││
│ │ group_registry.cleanup(&addr) ││ │ │ if extension installed: ││
│ │ notifications = ext.on_actor_death(&dead) ││
│ │ (StdExtension: emits Down/ActorExited, unregisters names, ││
│ │ removes from groups, takes monitors) ││
│ │ ext.cleanup_dead(&dead_addrs) ││
│ │ route notifications via route_to_pool_or_remote() ││
│ │ ││ │ │ ││
│ │ deliver any messages sent during on_stop callbacks ││ │ │ deliver any messages sent during on_stop callbacks ││
│ │ ││ │ │ ││
│ │ emit Down notifications for monitored dead actors: ││ │ │ worker_ext.gc_dead(&dead_addrs) ││
│ │ for (addr, reason) in dead: ││ │ │ (StdExtension: removes orphaned interval timers) ││
│ │ watchers = monitor_registry.take_monitors(&addr) ││
│ │ for each watcher: route Down { addr, reason } ││
│ │ same-worker → pool.deliver ││
│ │ cross-worker → transfer_tx + unpark ││
│ │ inbox → inbox_registry.try_deliver ││
│ │ monitor_registry.remove_watcher(&addr) ││
│ │ ││
│ │ timers.gc_dead_intervals(dead_addrs) ││
│ │ ││ │ │ ││
│ └────────────────────────────────────────────────────────────────────┘│ │ └────────────────────────────────────────────────────────────────────┘│
│ │ │ │
@ -510,13 +502,12 @@ Who holds what:
│ │ addr->wid │ │ load-aware │ │ addr->Sender│ │ AtomicBool │ │ │ │ addr->wid │ │ load-aware │ │ addr->Sender│ │ AtomicBool │ │
│ └─────┬─────┘ └──────┬─────┘ └──────┬──────┘ └──────┬─────┘ │ │ └─────┬─────┘ └──────┬─────┘ └──────┬──────┘ └──────┬─────┘ │
│ │ │ │ │ │ │ │ │ │ │ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ │ ┌─ extension: Arc<dyn RuntimeExtension> ──────────────────────────┐ │
│ │NameRegistry │ │MonitorRegist.│ │GroupRegistry │ │ │ │ StdExtension holds: NameRegistry, MonitorRegistry, │ │
│ │ name->addr │ │ watched-> │ │ group->addrs │ │ │ │ GroupRegistry, WatchRegistry (accessed via downcast) │ │
│ │ addr->name │ │ watchers │ │ addr->groups │ │ │ └─────────────────────────────────┬───────────────────────────────┘ │
│ └─────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │
│ │ │ │ │ │ ┌─────────────────────────────────┴──────────────────────────────────┐ │
│ ┌─────┴───────────────┴──────────────┴───────────────────────────────┐ │
│ │ TickContext (borrows all above) │ │ │ │ TickContext (borrows all above) │ │
│ └──────────────────────────┬──────────────────────────────────────────┘ │ │ └──────────────────────────┬──────────────────────────────────────────┘ │
│ │ │ │ │ │