4.6 KiB
4.6 KiB
Cycle 12: Named Actor Registry with Auto-Cleanup — Development History
Commit:
66a8523· 6 files · 267 insertions, 7 deletions
Motivation
Actors in swactor were only addressable by opaque ActorAddress values returned from spawn. There was no way to look up an actor by name — callers needed to pass addresses around manually. Named registration is one of the most fundamental actor runtime features, enabling service discovery within a runtime.
Competitor Analysis
| Framework | Key Type | Storage | Scope | Auto-Cleanup |
|---|---|---|---|---|
| Erlang | Atom | ETS table | Per-node or global | Yes (on process exit) |
| Actix | TypeId | SystemRegistry | Per-Arbiter | Yes (on actor stop) |
| Bastion | Path | Hierarchy | Global | Yes (structural) |
| Ractor | String | DashMap (global static) | Global | Yes (on actor death) |
| xactor | TypeId | Singleton registry | Global | N/A (singletons) |
| Akka | ServiceKey[T] | Receptionist | Cluster-wide | Yes (via DeathWatch) |
| Swactor | String | RwLock<HashMap> | Per-runtime | Yes (on death) |
Key Findings
- TypeId keys (Actix, xactor) don't fit swactor's type-erased model — multiple actors of the same type can't share a TypeId key
- Global static (Ractor) breaks multi-runtime scenarios (tests, embedding)
- Erlang's
register/whereisis the gold standard: atom keys, per-node scope, automatic cleanup on process exit
Implementation
NameRegistry
NameRegistryindelivery.rswith forward + reverse maps:names: RwLock<HashMap<String, ActorAddress>>— name → address lookupaddrs: RwLock<HashMap<ActorAddress, String>>— address → name (for O(1) cleanup)
- Added to
RuntimeasArc<NameRegistry>, threaded throughTickContext
Runtime API
spawn_named(name, actor)— spawn and register atomicallywhere_is(name)— look up address by nameunregister(name)— manual unregistration (actor keeps running)registered_names()— list all registered names
Context API
ctx.spawn_named(name, actor)— register from within a handlerctx.where_is(name)— look up from within a handler
Auto-Cleanup
cleanup_deadphase callsname_registry.unregister_by_addr()for each dead actor- Name is freed immediately — can be reused for a replacement actor
TOCTOU Prevention
- Name reservation is immediate (before spawn queue push) — prevents race between checking name availability and registering it
Key files modified: src/delivery.rs, src/runtime.rs, src/actor.rs, src/worker.rs, tests/runtime_api.rs
Design Decisions
- String keys — most flexible. Atoms (Erlang) aren't idiomatic in Rust. TypeId (Actix) is too restrictive. Strings allow any naming convention.
- Per-runtime scope — matches swactor's architecture (one runtime per application). Global registries (Ractor) cause problems in tests and embedded scenarios.
- RwLock<HashMap> — matches the existing
AddressMapandInboxRegistrypattern. RwLock allows concurrent reads (lookups) with exclusive writes (registration). - Collision returns error —
spawn_namedreturnsErrif the name is already taken. The original binding is preserved. This is explicit and predictable, matching Erlang's behavior. - Reverse map for O(1) cleanup — without the reverse map, cleanup would require scanning all entries. The reverse map adds memory proportional to registered actors but makes cleanup constant-time.
- Immediate reservation — name is reserved before the spawn is queued, preventing TOCTOU races where two
spawn_namedcalls for the same name could both succeed.
Tests Added
11 new tests (95 → 106 total):
named_actor_lookup_returns_spawn_address— spawn_named → where_is roundtripnamed_actor_receives_messages_via_lookup— send to looked-up address worksduplicate_name_returns_error— collision error, original binding preservedwhere_is_returns_none_for_unknown_name— nonexistent name → Nonename_auto_unregistered_on_actor_death— stop_actor → name freedname_can_be_reused_after_actor_death— death → respawn with same name succeedsname_auto_unregistered_on_panic— panic → name freedregistered_names_lists_all— all registered names returnedmanual_unregister_frees_name_but_actor_lives— unregister doesn't kill the actorctx_where_is_resolves_inside_handler— where_is works from handler contextctx_spawn_named_registers_from_handler— spawn_named works from handler context
Result
- 106 tests pass (99 behavioral + 7 proptest)
- All workspace crates compile, zero warnings