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>
Add proptest-state-machine for stateful property testing of the runtime,
covering FIFO ordering, budget fairness, timer correctness, mailbox bounds,
and spawn tracking. Extend cargo-fuzz with stop, restart, and timer actions.
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>
Adds some basic benchmarking, stress tests. They still need to be properly examined to ensure they are testing the correct properties, but fit for "good enough". Implements the HybridChannel type, which features a channel buffer that can withstand overflows. It does so by providing a dequeue behind a mutex. Without overflow, will push messages into the lock free ArrayQueue implemented by crossbeam_queue; when that buffer fills, will use the locking portion provided by the Mutex<VecDequeue>.
In the future we can even further optimize this, perhaps with some linked list implementations of lock-free channels, but, like the benchmarks, this fits the "good enough" bar for now.