84 lines
4.6 KiB
Markdown
84 lines
4.6 KiB
Markdown
|
|
# 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/whereis`** is the gold standard: atom keys, per-node scope, automatic cleanup on process exit
|
||
|
|
|
||
|
|
## Implementation
|
||
|
|
|
||
|
|
### NameRegistry
|
||
|
|
- `NameRegistry` in `delivery.rs` with forward + reverse maps:
|
||
|
|
- `names: RwLock<HashMap<String, ActorAddress>>` — name → address lookup
|
||
|
|
- `addrs: RwLock<HashMap<ActorAddress, String>>` — address → name (for O(1) cleanup)
|
||
|
|
- Added to `Runtime` as `Arc<NameRegistry>`, threaded through `TickContext`
|
||
|
|
|
||
|
|
### Runtime API
|
||
|
|
- `spawn_named(name, actor)` — spawn and register atomically
|
||
|
|
- `where_is(name)` — look up address by name
|
||
|
|
- `unregister(name)` — manual unregistration (actor keeps running)
|
||
|
|
- `registered_names()` — list all registered names
|
||
|
|
|
||
|
|
### Context API
|
||
|
|
- `ctx.spawn_named(name, actor)` — register from within a handler
|
||
|
|
- `ctx.where_is(name)` — look up from within a handler
|
||
|
|
|
||
|
|
### Auto-Cleanup
|
||
|
|
- `cleanup_dead` phase calls `name_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 `AddressMap` and `InboxRegistry` pattern. RwLock allows concurrent reads (lookups) with exclusive writes (registration).
|
||
|
|
- **Collision returns error** — `spawn_named` returns `Err` if 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_named` calls 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 roundtrip
|
||
|
|
- `named_actor_receives_messages_via_lookup` — send to looked-up address works
|
||
|
|
- `duplicate_name_returns_error` — collision error, original binding preserved
|
||
|
|
- `where_is_returns_none_for_unknown_name` — nonexistent name → None
|
||
|
|
- `name_auto_unregistered_on_actor_death` — stop_actor → name freed
|
||
|
|
- `name_can_be_reused_after_actor_death` — death → respawn with same name succeeds
|
||
|
|
- `name_auto_unregistered_on_panic` — panic → name freed
|
||
|
|
- `registered_names_lists_all` — all registered names returned
|
||
|
|
- `manual_unregister_frees_name_but_actor_lives` — unregister doesn't kill the actor
|
||
|
|
- `ctx_where_is_resolves_inside_handler` — where_is works from handler context
|
||
|
|
- `ctx_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
|