3.8 KiB
Cycle 7: Actor Recovery via Factory-Based Restart — Development History
Commit:
1779ad6· 6 files · 167 insertions, 10 deletions
Motivation
Before this change, a panicking actor was permanently poisoned — it could never process messages again. Its address remained in the address map but silently discarded all messages. In production, this means a single panic permanently degrades the system. Every mature actor framework provides some form of crash recovery.
Competitor Analysis
| Framework | Recovery Model | State After Restart | Mailbox After Restart |
|---|---|---|---|
| Erlang/OTP | Factory (MFA tuple), fresh process | Fresh (new init/1) | Lost (new PID) |
| Akka | Replace internals, keep ActorRef | Fresh (preRestart hook) | Preserved (docs say "usually wrong") |
| Kameo | on_panic(&mut self) hook |
Potentially corrupt | Preserved |
| Actix | Supervised trait, re-create context |
Fresh | Lost |
| Ractor | SupervisionEvent callback |
Up to supervisor | Up to supervisor |
| Swactor (before) | None — permanent poison | N/A | Silently discarded |
Key Insight
Akka's approach of preserving state by replacing internals is documented as "usually wrong" — the state that caused the panic is likely corrupt. Kameo's on_panic(&mut self) is risky for the same reason. Erlang's factory-based restart (fresh process from MFA tuple) is the safest approach: guaranteed clean state.
Implementation
Actor<A>expanded from tuple struct to named fields:inner,restart_factory,max_restarts,restart_countAnyActor::try_restart(&self) -> Option<Box<dyn AnyActor>>trait method (defaultNone, backward compatible)- Factory stored as
Arc<dyn Fn() -> A + Send + Sync>— called to produce fresh actor instance on restart spawn_restartable(actor, factory, max_restarts)added to bothRuntimeandCtxtick_allpanic handler:try_restart()before poisoning; on success, replace actor, clear mailbox, reset staterestartscounter added toWorkerStatsandWorkerInfo
Key files modified: src/actor.rs, src/runtime.rs, src/worker.rs, src/stats.rs, tests/runtime_api.rs
Design Decisions
- Factory-based restart (Erlang model) — safest approach, guaranteed clean state. Factory closure is
Arc<dyn Fn() -> A>, cloned into freshActor<A>on each restart. - max_restarts limit — prevents infinite restart loops. When exceeded, actor is permanently poisoned. Mirrors Erlang's restart intensity.
- Mailbox cleared on restart — messages that triggered the panic are discarded. Fresh actor starts with empty mailbox. (Erlang does this too — new PID means new mailbox.)
- Same address preserved — unlike Erlang (new PID), the restarted actor keeps its
ActorAddress. This is simpler for callers and matches Akka's model. - Factory fields are "cold" —
restart_factoryandmax_restartsare never touched byhandle_any(the hot path). Aftercatch_unwind, these fields are guaranteed safe to read. - Non-restartable actors unchanged —
try_restart()returnsNoneby default, preserving the existing poison-on-panic behavior.
Tests Added
4 new tests (64 → 68 total):
restartable_actor_recovers_after_panic— basic restart works: panic, recover, process new messagesrestartable_actor_resets_state_on_restart— fresh state confirmed post-restart (counter resets to zero)restartable_actor_respects_max_restarts— 2 restarts allowed, 3rd panic → permanent poisonnon_restartable_actor_still_poisons_on_panic— backward compatibility: default actors still poison
Result
- 68 tests pass
- All workspace crates compile
- Swactor weakness "panicked actors permanently poisoned" resolved
- Foundation laid for supervision trees (Cycle 17)