Add coordinated restart strategies to the Supervisor actor. OneForAll
restarts all children when one fails; RestForOne restarts the failed
child and all children after it in spec order. Uses a SupervisorPhase
state machine to coordinate stop signals and Down confirmations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add three features to enable Erlang-style supervision:
1. ActorInterface::handle_down() — callback for monitored actor deaths,
allowing actors to react to Down messages without making Down their
Incoming type. Backward-compatible: actors with Incoming=Down still
receive through handle().
2. ctx.stop_actor(addr) — send graceful stop to another actor from
handler context using PoisonPill semantics.
3. Supervisor actor — manages child actors with configurable restart
policies (Permanent/Transient/Temporary), OneForOne strategy, and
meltdown detection (max_restarts). Built entirely on existing
primitives (monitor, spawn, Down, lifecycle hooks).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Runtime::ask() and Ask<R> wrapper for convenient request-response.
Creates a temporary inbox, sends the request (with reply address via
closure), and provides recv_ticking() for automatic tick-until-response.
Purely sugar over the existing inbox pattern — no implicit auto-reply.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add GroupRegistry for named actor groups. Actors join/leave groups via
rt.join_group() / ctx.join_group(), and messages can be broadcast to all
members via rt.publish_to() / ctx.publish(). Groups auto-create on first
join, auto-delete when empty, and members are auto-removed on death.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add MonitorRegistry for death watch subscriptions. Actors subscribe via
ctx.monitor(target) and receive a Down { addr, reason } message when the
target dies (stop or panic). Supports stacking, demonitor, and automatic
cleanup of dead watcher subscriptions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add NameRegistry (String → ActorAddress) to delivery.rs with forward and
reverse maps for O(1) lookup and cleanup. Actors can be spawned with names
via rt.spawn_named() / ctx.spawn_named(), looked up via where_is(), and
names are automatically freed when actors stop or panic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds deterministic tick-based timer scheduling to the actor runtime.
Timers are per-worker (no cross-thread sync) and tick-counted (not
wall-clock), making them suitable for simulation and testing.
API: ctx.send_after_ticks() for one-shot, ctx.send_interval_ticks()
for repeating timers. Timer messages route through the full delivery
system (local pool, cross-worker transfer, and external inboxes).
Fixes over-aggressive interval timer GC that incorrectly removed
timers targeting inboxes and cross-worker actors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add actor lifecycle hooks and two-mode graceful stop mechanism:
- ActorInterface::on_start() called once before first message (panic = poison)
- ActorInterface::on_stop() called on graceful stop (NOT on panic - unsafe)
- ctx.stop_self() for immediate self-stop after current message
- runtime.stop_actor() for external stop (PoisonPill semantics, queued)
- Separate stats tracking: stops counter distinct from panics
- 12 new behavioral tests, 82 total passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Poisoned actors are now removed from ActorPool and AddressMap after
tick_all (phase 7). Previously they leaked indefinitely. Sends to
cleaned-up actors return Err instead of silently discarding — callers
learn the actor is gone.
Known bug class in Akka (#22990), C++ Actor Framework (#420).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add spawn_restartable(actor, factory, max_restarts) to Runtime and Ctx.
On panic, the actor is recreated using the factory with fresh state and
cleared mailbox. After max_restarts exhausted, permanently poisoned.
AnyActor::try_restart() trait method with backward-compatible default.
Factory stored as Arc<dyn Fn() -> A + Send + Sync> — safe to read after
catch_unwind since factory fields are never touched by handle_any.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add optional bounded mailboxes to prevent unbounded memory growth.
MailboxOverflow enum: DropNewest (discard incoming) or DropOldest
(evict oldest to make room). Default capacity=0 preserves unbounded
behavior for full backward compatibility.
messages_dropped counter added to WorkerStats for observability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace blind round-robin placement with load-aware strategy that reads
per-worker stats (actor count + mailbox depth) and biases toward lighter
workers. Falls back to round-robin when stats are equal (initial burst).
Deep research on work stealing across Tokio (steal-half, LIFO slot),
Go (M:N scheduler), BEAM (proactive migration), and ForkJoinPool.
Full actor migration is feasible but deferred due to message-loss window.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shutdown() now wakes all parked workers immediately for fast exit.
5 new tests from competitor bug patterns: ractor #310 (destructive
snapshots), kameo #185 (startup delivery), actix #515 (mailbox bypass),
plus stats-under-load and shutdown-wakes-parked validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace thread::sleep with thread::park_timeout in worker backoff loop.
Workers register their thread handle via OnceLock<Thread> on startup.
When send_to or spawn routes work to a worker, Thread::unpark() wakes
it instantly instead of waiting for the sleep timer to expire.
Inspired by tokio's parker state machine and Linux NO_HZ adaptive ticks.
Implementation:
- Runtime stores Vec<OnceLock<Thread>> for worker thread handles
- Workers call OnceLock::set(thread::current()) on startup
- Runtime::send_any, spawn_any, and WorkerContext cross-worker sends
call notify_worker() → Thread::unpark() on the target worker
- TickContext carries worker_threads reference for cross-worker notification
- Zero new dependencies (std::sync::OnceLock + std:🧵:park_timeout)
Benefits:
- Parked workers wake instantly when work arrives (vs up to 1ms sleep delay)
- No overhead on hot path — unpark() is no-op if thread isn't parked
- Single-threaded tick() mode unaffected (OnceLock never set)
All 58 tests pass (52 runtime_api + 5 transport + 1 doctest).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Research across ractor, tokio, Erlang/OTP BEAM, Linux CFS, and libuv
revealed that tick_all drained the entire mailbox per actor per tick,
allowing one hot actor to starve all others on the same worker.
- Add `actor_message_budget` to RuntimeConfig (default: 64 msgs/actor/tick)
- Modify tick_all to break after budget messages, yielding to next actor
- budget=0 restores unlimited (backward compatible) behavior
- 3 new fairness tests validating hot-cold actor scenarios
- New fairness benchmark group (cold_latency_under_pressure, throughput_by_budget)
- Fix RuntimeConfig struct literals across workspace crates
Inspired by BEAM's 4000-reduction budget and tokio's 128-op cooperative budget.
All 45 tests pass (42 original + 3 new).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>