feat: named actor registry with auto-cleanup on death (Cycle 12)
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>
This commit is contained in:
parent
9b1518b46c
commit
66a8523473
6 changed files with 384 additions and 5 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Current Stage: Phase 1 — Research + First Improvement Cycle
|
||||
|
||||
### Status: Cycle 11 COMPLETE
|
||||
### Status: Cycle 12 COMPLETE
|
||||
|
||||
## Plan Overview
|
||||
1. **Phase 0**: Codebase audit — understand current swactor architecture, existing tests, benchmarks ✅
|
||||
|
|
@ -124,6 +124,38 @@
|
|||
- `stop_nonexistent_actor_returns_error` — stop on bad address returns Err
|
||||
- **Result**: 82 tests pass, all workspace compiles
|
||||
|
||||
### Cycle 12: Named Actor Registry
|
||||
- **Research**: Studied named actor/service discovery across Erlang (register/2, whereis/1, global, pg),
|
||||
Actix (Registry, SystemRegistry — TypeId keys), Bastion (hierarchy-based), Ractor (String keys, DashMap,
|
||||
global static), xactor (TypeId singleton), Akka (Receptionist, ServiceKey[T])
|
||||
- Key findings: TypeId keys (Actix/xactor) don't fit swactor's type-erased model; global static
|
||||
(Ractor) breaks multi-runtime scenarios; Erlang's register/whereis is the gold standard
|
||||
- Decision: String keys, RwLock<HashMap> (matches existing AddressMap/InboxRegistry pattern),
|
||||
per-runtime scope, error on collision, auto-unregister on death
|
||||
- **Implementation**: `NameRegistry` in delivery.rs with forward + reverse maps
|
||||
- `NameRegistry`: `RwLock<HashMap<String, ActorAddress>>` + `RwLock<HashMap<ActorAddress, String>>`
|
||||
- Forward map for O(1) name→addr lookup, reverse map for O(1) addr→name cleanup
|
||||
- Added to `Runtime` as `Arc<NameRegistry>`, threaded through `TickContext`
|
||||
- Runtime API: `spawn_named(name, actor)`, `where_is(name)`, `unregister(name)`, `registered_names()`
|
||||
- Ctx API: `spawn_named(name, actor)`, `where_is(name)` — usable from inside handlers
|
||||
- `ContextInner` trait extended: `where_is()` + `register_name()` (private, supports both Runtime and WorkerContext)
|
||||
- Auto-unregister on death: `cleanup_dead` phase calls `name_registry.unregister_by_addr()` for each dead actor
|
||||
- Name reservation is immediate (before spawn queue push) — prevents TOCTOU race
|
||||
- Collision returns `Err("Name already registered")` — original binding preserved
|
||||
- **Tests**: 11 new behavioral tests
|
||||
- `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 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
|
||||
- `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 actor
|
||||
- `ctx_where_is_resolves_inside_handler` — where_is from handler context
|
||||
- `ctx_spawn_named_registers_from_handler` — spawn_named from handler context
|
||||
- **Result**: 106 tests pass (99 behavioral + 7 proptest), all workspace compiles, zero warnings
|
||||
|
||||
### Cycle 11: Property-Based Testing (proptest + fuzz extension)
|
||||
- **Research**: Studied testing approaches across tokio (loom), Erlang (PropEr, QuickCheck, Concuerror),
|
||||
Rust property-based testing (proptest vs quickcheck), cargo-fuzz, and actor-specific testing patterns.
|
||||
|
|
|
|||
27
src/actor.rs
27
src/actor.rs
|
|
@ -178,6 +178,10 @@ pub trait ContextInner {
|
|||
fn request_stop(&self, addr: ActorAddress);
|
||||
/// Schedule a timer (one-shot or interval).
|
||||
fn schedule_timer(&self, request: TimerRequest);
|
||||
/// Look up an actor address by registered name.
|
||||
fn where_is(&self, name: &str) -> Option<ActorAddress>;
|
||||
/// Register a name → address mapping. Returns `Err` if the name is taken.
|
||||
fn register_name(&self, name: String, addr: ActorAddress) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Actor syscall interface — passed to `ActorInterface::handle()`.
|
||||
|
|
@ -249,6 +253,29 @@ impl<'a> Ctx<'a> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Look up an actor address by its registered name.
|
||||
///
|
||||
/// Returns `None` if no actor is registered under that name.
|
||||
pub fn where_is(&self, name: &str) -> Option<ActorAddress> {
|
||||
self.inner.where_is(name)
|
||||
}
|
||||
|
||||
/// Spawn a new actor with a registered name.
|
||||
///
|
||||
/// The name is reserved immediately (before the actor starts processing).
|
||||
/// Returns `Err` if the name is already taken.
|
||||
pub fn spawn_named<A: ActorInterface>(
|
||||
&self,
|
||||
name: impl Into<String>,
|
||||
actor: A,
|
||||
) -> Result<ActorAddress, Error> {
|
||||
let addr = ActorAddress::new_random();
|
||||
self.inner.register_name(name.into(), addr)?;
|
||||
let boxed: Box<dyn AnyActor> = Box::new(Actor::new(actor));
|
||||
self.inner.spawn_any(addr, boxed);
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Spawn a restartable actor. On panic, recreated via `factory` up to
|
||||
/// `max_restarts` times before permanent poisoning.
|
||||
pub fn spawn_restartable<A, F>(
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ pub(crate) struct TickContext<'a> {
|
|||
pub(crate) placement: &'a Placement,
|
||||
pub(crate) inbox_registry: &'a InboxRegistry,
|
||||
pub(crate) config: &'a RuntimeConfig,
|
||||
pub(crate) name_registry: &'a NameRegistry,
|
||||
pub(crate) stats_hook: Option<&'a dyn crate::stats::StatsHook>,
|
||||
/// Thread handles for waking parked workers on cross-worker sends.
|
||||
pub(crate) worker_threads: &'a [OnceLock<Thread>],
|
||||
|
|
@ -198,6 +199,62 @@ pub(crate) struct TickContext<'a> {
|
|||
pub(crate) transport_router: Option<&'a crate::transport::TransportRouter>,
|
||||
}
|
||||
|
||||
// ─── Name Registry ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Named actor registry — maps human-readable names to actor addresses.
|
||||
///
|
||||
/// `RwLock<HashMap>` — same pattern as `AddressMap`. Write-rare (spawn/death),
|
||||
/// read-often (lookup). A reverse map enables O(1) cleanup on actor death.
|
||||
pub(crate) struct NameRegistry {
|
||||
names: RwLock<HashMap<String, ActorAddress>>,
|
||||
reverse: RwLock<HashMap<ActorAddress, String>>,
|
||||
}
|
||||
|
||||
impl NameRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
names: RwLock::new(HashMap::new()),
|
||||
reverse: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a name → address mapping. Returns `Err` if the name is already taken.
|
||||
pub fn register(&self, name: String, addr: ActorAddress) -> Result<(), crate::Error> {
|
||||
let mut names = self.names.write().unwrap();
|
||||
if names.contains_key(&name) {
|
||||
return Err(crate::Error::from("Name already registered"));
|
||||
}
|
||||
names.insert(name.clone(), addr);
|
||||
drop(names);
|
||||
self.reverse.write().unwrap().insert(addr, name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up an actor address by name.
|
||||
pub fn lookup(&self, name: &str) -> Option<ActorAddress> {
|
||||
self.names.read().unwrap().get(name).copied()
|
||||
}
|
||||
|
||||
/// Unregister a name, returning the address it was bound to.
|
||||
pub fn unregister(&self, name: &str) -> Option<ActorAddress> {
|
||||
let addr = self.names.write().unwrap().remove(name)?;
|
||||
self.reverse.write().unwrap().remove(&addr);
|
||||
Some(addr)
|
||||
}
|
||||
|
||||
/// Remove a name by address (called on actor death for auto-cleanup).
|
||||
pub fn unregister_by_addr(&self, addr: &ActorAddress) {
|
||||
if let Some(name) = self.reverse.write().unwrap().remove(addr) {
|
||||
self.names.write().unwrap().remove(&name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all registered names.
|
||||
pub fn registered_names(&self) -> Vec<String> {
|
||||
self.names.read().unwrap().keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TickContext<'a> {
|
||||
/// Route a message whose destination is not in the local address map.
|
||||
/// Tries inbox registry, then remote transport, then falls back to inbox error.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Message, StopS
|
|||
use crate::channel::{Receiver, Sender};
|
||||
// Re-export config types so existing code using `runtime::RuntimeConfig` still works
|
||||
pub use crate::config::{BackoffPolicy, MailboxOverflow, RuntimeConfig};
|
||||
use crate::delivery::{AddressMap, Envelope, InboxRegistry, Placement, TickContext, WorkerId};
|
||||
use crate::delivery::{AddressMap, Envelope, InboxRegistry, NameRegistry, Placement, TickContext, WorkerId};
|
||||
use crate::stats::{StatsHook, WorkerStats};
|
||||
// Re-export stats types so existing code using `runtime::*` still works
|
||||
pub use crate::stats::{RuntimeStats, WorkerInfo};
|
||||
|
|
@ -62,6 +62,7 @@ pub struct Runtime {
|
|||
config: RuntimeConfig,
|
||||
address_map: Arc<AddressMap>,
|
||||
inbox_registry: Arc<InboxRegistry>,
|
||||
name_registry: Arc<NameRegistry>,
|
||||
transfer_txs: Vec<Sender<Envelope>>,
|
||||
spawn_txs: Vec<Sender<(ActorAddress, Box<dyn AnyActor>)>>,
|
||||
placement: Placement,
|
||||
|
|
@ -119,6 +120,7 @@ impl Runtime {
|
|||
|
||||
let address_map = Arc::new(AddressMap::with_capacity(config.max_actors));
|
||||
let inbox_registry = Arc::new(InboxRegistry::new());
|
||||
let name_registry = Arc::new(NameRegistry::new());
|
||||
|
||||
let mut transfer_txs = Vec::with_capacity(num_workers);
|
||||
let mut spawn_txs = Vec::with_capacity(num_workers);
|
||||
|
|
@ -156,6 +158,7 @@ impl Runtime {
|
|||
config,
|
||||
address_map,
|
||||
inbox_registry,
|
||||
name_registry,
|
||||
transfer_txs,
|
||||
spawn_txs,
|
||||
placement,
|
||||
|
|
@ -226,6 +229,38 @@ impl Runtime {
|
|||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Spawn an actor with a registered name, returns its address.
|
||||
///
|
||||
/// The name is reserved immediately. Returns `Err` if the name is already taken.
|
||||
pub fn spawn_named<A: ActorInterface>(
|
||||
&self,
|
||||
name: impl Into<String>,
|
||||
actor: A,
|
||||
) -> Result<ActorAddress, Error> {
|
||||
let addr = ActorAddress::new_random();
|
||||
self.name_registry.register(name.into(), addr)?;
|
||||
let worker_id = self.placement.next_worker();
|
||||
self.address_map.insert(addr, worker_id);
|
||||
let boxed: Box<dyn AnyActor> = Box::new(Actor::new(actor));
|
||||
self.spawn_txs[worker_id.as_usize()].send((addr, boxed));
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Look up an actor address by its registered name.
|
||||
pub fn where_is(&self, name: &str) -> Option<ActorAddress> {
|
||||
self.name_registry.lookup(name)
|
||||
}
|
||||
|
||||
/// Unregister a name. Returns the address it was bound to, or `None`.
|
||||
pub fn unregister(&self, name: &str) -> Option<ActorAddress> {
|
||||
self.name_registry.unregister(name)
|
||||
}
|
||||
|
||||
/// Return all currently registered actor names.
|
||||
pub fn registered_names(&self) -> Vec<String> {
|
||||
self.name_registry.registered_names()
|
||||
}
|
||||
|
||||
/// Send a message to an actor address
|
||||
pub fn send_to<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
|
||||
let result = self.send_any(addr, Box::new(msg));
|
||||
|
|
@ -256,6 +291,7 @@ impl Runtime {
|
|||
placement: &self.placement,
|
||||
inbox_registry: &self.inbox_registry,
|
||||
config: &self.config,
|
||||
name_registry: &self.name_registry,
|
||||
stats_hook: self.stats_hook.as_deref(),
|
||||
worker_threads: &self.worker_threads,
|
||||
#[cfg(feature = "transport")]
|
||||
|
|
@ -456,4 +492,12 @@ impl ContextInner for Runtime {
|
|||
// Use rt.send_to() with a delay loop instead.
|
||||
eprintln!("swactor: schedule_timer called outside worker context — ignored");
|
||||
}
|
||||
|
||||
fn where_is(&self, name: &str) -> Option<ActorAddress> {
|
||||
self.name_registry.lookup(name)
|
||||
}
|
||||
|
||||
fn register_name(&self, name: String, addr: ActorAddress) -> Result<(), Error> {
|
||||
self.name_registry.register(name, addr)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ impl Worker {
|
|||
if !dead.is_empty() {
|
||||
for addr in &dead {
|
||||
tc.address_map.remove(addr);
|
||||
tc.name_registry.unregister_by_addr(addr);
|
||||
}
|
||||
// Re-publish num_actors after cleanup so stats reflect removal
|
||||
self.stats.num_actors.store(self.pool.len(), Ordering::Relaxed);
|
||||
|
|
@ -418,6 +419,14 @@ impl ContextInner for WorkerContext<'_> {
|
|||
fn schedule_timer(&self, request: TimerRequest) {
|
||||
self.timer_requests.borrow_mut().push(request);
|
||||
}
|
||||
|
||||
fn where_is(&self, name: &str) -> Option<ActorAddress> {
|
||||
self.tc.name_registry.lookup(name)
|
||||
}
|
||||
|
||||
fn register_name(&self, name: String, addr: ActorAddress) -> Result<(), crate::Error> {
|
||||
self.tc.name_registry.register(name, addr)
|
||||
}
|
||||
}
|
||||
|
||||
struct ActorSlot {
|
||||
|
|
|
|||
|
|
@ -2684,9 +2684,9 @@ impl ActorInterface for TimerStartActor {
|
|||
}
|
||||
|
||||
/// Actor that schedules a one-shot timer when it receives a Forward message.
|
||||
struct DelayEchoActor;
|
||||
struct DelayPingPongActor;
|
||||
|
||||
impl ActorInterface for DelayEchoActor {
|
||||
impl ActorInterface for DelayPingPongActor {
|
||||
type Incoming = Forward;
|
||||
type Response = Done;
|
||||
|
||||
|
|
@ -2751,7 +2751,7 @@ fn handler_can_schedule_one_shot_timer() {
|
|||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<Done>().unwrap();
|
||||
|
||||
let addr = rt.spawn(DelayEchoActor).unwrap();
|
||||
let addr = rt.spawn(DelayPingPongActor).unwrap();
|
||||
|
||||
let _ = rt.send_to(addr, Forward { value: 42, reply_to: *inbox.addr() });
|
||||
rt.tick(); // process Forward, schedule timer (delay=3)
|
||||
|
|
@ -2876,3 +2876,213 @@ fn timer_with_zero_delay_fires_next_tick() {
|
|||
rt.tick(); // timer fires
|
||||
assert!(inbox.try_recv().is_some(), "zero-delay timer fires on next tick");
|
||||
}
|
||||
|
||||
// ── Named Actor Registry ────────────────────────────────────────────────────
|
||||
|
||||
/// Given a named actor is spawned,
|
||||
/// when I look it up by name,
|
||||
/// then I get the same address that spawn returned.
|
||||
#[test]
|
||||
fn named_actor_lookup_returns_spawn_address() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let addr = rt.spawn_named("greeter", PingPongActor).unwrap();
|
||||
assert_eq!(rt.where_is("greeter"), Some(addr));
|
||||
}
|
||||
|
||||
/// Given a named actor exists,
|
||||
/// when I send a message to the looked-up address,
|
||||
/// then the actor receives and processes it.
|
||||
#[test]
|
||||
fn named_actor_receives_messages_via_lookup() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<Pong>().unwrap();
|
||||
let addr = rt.spawn_named("ponger", PingPongActor).unwrap();
|
||||
assert_eq!(rt.where_is("ponger"), Some(addr));
|
||||
|
||||
rt.send_to(addr, Ping { reply_to: *inbox.addr() }).unwrap();
|
||||
rt.tick();
|
||||
assert!(inbox.try_recv().is_some(), "named actor should process message");
|
||||
}
|
||||
|
||||
/// Given a name is already registered,
|
||||
/// when I try to spawn another actor with the same name,
|
||||
/// then I get an error and the original binding is preserved.
|
||||
#[test]
|
||||
fn duplicate_name_returns_error() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let first_addr = rt.spawn_named("singleton", PingPongActor).unwrap();
|
||||
let result = rt.spawn_named("singleton", PingPongActor);
|
||||
assert!(result.is_err(), "duplicate name should fail");
|
||||
assert_eq!(rt.where_is("singleton"), Some(first_addr), "original binding preserved");
|
||||
}
|
||||
|
||||
/// Given no actors are registered,
|
||||
/// when I look up a nonexistent name,
|
||||
/// then I get None.
|
||||
#[test]
|
||||
fn where_is_returns_none_for_unknown_name() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
assert_eq!(rt.where_is("ghost"), None);
|
||||
}
|
||||
|
||||
/// Given a named actor is stopped,
|
||||
/// when the next tick runs cleanup,
|
||||
/// then the name is automatically unregistered.
|
||||
#[test]
|
||||
fn name_auto_unregistered_on_actor_death() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let addr = rt.spawn_named("ephemeral", PingPongActor).unwrap();
|
||||
rt.tick(); // on_start
|
||||
|
||||
rt.stop_actor(addr).unwrap();
|
||||
rt.tick(); // process StopSignal + cleanup
|
||||
|
||||
assert_eq!(rt.where_is("ephemeral"), None, "name should be freed after stop");
|
||||
}
|
||||
|
||||
/// Given a named actor died and its name was freed,
|
||||
/// when I spawn a new actor with the same name,
|
||||
/// then registration succeeds with a new address.
|
||||
#[test]
|
||||
fn name_can_be_reused_after_actor_death() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let first = rt.spawn_named("worker", PingPongActor).unwrap();
|
||||
rt.tick();
|
||||
rt.stop_actor(first).unwrap();
|
||||
rt.tick(); // cleanup frees the name
|
||||
|
||||
let second = rt.spawn_named("worker", PingPongActor).unwrap();
|
||||
assert_ne!(first, second, "new actor should have a different address");
|
||||
assert_eq!(rt.where_is("worker"), Some(second));
|
||||
}
|
||||
|
||||
/// Given a named actor panics (and is not restartable),
|
||||
/// when the next tick runs cleanup,
|
||||
/// then the name is freed.
|
||||
#[test]
|
||||
fn name_auto_unregistered_on_panic() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<Pong>().unwrap();
|
||||
let _addr = rt.spawn_named("fragile", PanicActor).unwrap();
|
||||
rt.tick(); // on_start
|
||||
|
||||
rt.send_to(_addr, PanicMsg).unwrap();
|
||||
rt.tick(); // panic → poison → cleanup
|
||||
|
||||
assert_eq!(rt.where_is("fragile"), None, "name freed after panic");
|
||||
// Can reuse the name
|
||||
let _new = rt.spawn_named("fragile", PingPongActor).unwrap();
|
||||
assert!(rt.where_is("fragile").is_some());
|
||||
drop(inbox);
|
||||
}
|
||||
|
||||
/// Given multiple named actors are registered,
|
||||
/// when I call registered_names(),
|
||||
/// then all names are returned.
|
||||
#[test]
|
||||
fn registered_names_lists_all() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
rt.spawn_named("alpha", PingPongActor).unwrap();
|
||||
rt.spawn_named("beta", PingPongActor).unwrap();
|
||||
rt.spawn_named("gamma", PingPongActor).unwrap();
|
||||
|
||||
let mut names = rt.registered_names();
|
||||
names.sort();
|
||||
assert_eq!(names, vec!["alpha", "beta", "gamma"]);
|
||||
}
|
||||
|
||||
/// Given a named actor exists,
|
||||
/// when I manually unregister the name,
|
||||
/// then the name is freed but the actor continues running.
|
||||
#[test]
|
||||
fn manual_unregister_frees_name_but_actor_lives() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<Pong>().unwrap();
|
||||
let addr = rt.spawn_named("temp-name", PingPongActor).unwrap();
|
||||
rt.tick(); // on_start
|
||||
|
||||
let removed = rt.unregister("temp-name");
|
||||
assert_eq!(removed, Some(addr));
|
||||
assert_eq!(rt.where_is("temp-name"), None, "name freed");
|
||||
|
||||
// Actor still alive and can receive messages
|
||||
rt.send_to(addr, Ping { reply_to: *inbox.addr() }).unwrap();
|
||||
rt.tick();
|
||||
assert!(inbox.try_recv().is_some(), "actor still processes messages");
|
||||
}
|
||||
|
||||
/// An actor that looks up a peer by name using ctx.where_is().
|
||||
struct NameLookupActor {
|
||||
target_name: &'static str,
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
impl ActorInterface for NameLookupActor {
|
||||
type Incoming = Ping;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
|
||||
if let Some(peer) = ctx.where_is(self.target_name) {
|
||||
ctx.send(self.reply_to, MyAddr(peer)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a named actor exists,
|
||||
/// when another actor calls ctx.where_is() from inside a handler,
|
||||
/// then it resolves the correct address.
|
||||
#[test]
|
||||
fn ctx_where_is_resolves_inside_handler() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<MyAddr>().unwrap();
|
||||
let target = rt.spawn_named("target", PingPongActor).unwrap();
|
||||
|
||||
let looker = rt.spawn(NameLookupActor {
|
||||
target_name: "target",
|
||||
reply_to: *inbox.addr(),
|
||||
}).unwrap();
|
||||
|
||||
rt.tick(); // on_start
|
||||
rt.send_to(looker, Ping { reply_to: ActorAddress::default() }).unwrap();
|
||||
rt.tick(); // handle → where_is → send
|
||||
rt.tick(); // deliver reply
|
||||
|
||||
let result = inbox.try_recv();
|
||||
assert_eq!(result, Some(MyAddr(target)), "ctx.where_is found the named actor");
|
||||
}
|
||||
|
||||
/// An actor that spawns a named child using ctx.spawn_named().
|
||||
struct NamedSpawnerActor {
|
||||
reply_to: ActorAddress,
|
||||
}
|
||||
|
||||
impl ActorInterface for NamedSpawnerActor {
|
||||
type Incoming = Ping;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
|
||||
match ctx.spawn_named("child", PingPongActor) {
|
||||
Ok(addr) => { ctx.send(self.reply_to, MyAddr(addr)).unwrap(); }
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an actor calls ctx.spawn_named("child", ...),
|
||||
/// when the child is spawned,
|
||||
/// then where_is("child") returns the correct address.
|
||||
#[test]
|
||||
fn ctx_spawn_named_registers_from_handler() {
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<MyAddr>().unwrap();
|
||||
|
||||
let spawner = rt.spawn(NamedSpawnerActor {
|
||||
reply_to: *inbox.addr(),
|
||||
}).unwrap();
|
||||
|
||||
rt.tick(); // on_start
|
||||
rt.send_to(spawner, Ping { reply_to: ActorAddress::default() }).unwrap();
|
||||
rt.tick(); // handle → spawn_named
|
||||
rt.tick(); // deliver reply
|
||||
|
||||
let child_addr = inbox.try_recv().expect("should receive child address");
|
||||
assert_eq!(rt.where_is("child"), Some(child_addr.0), "name registered from handler");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue