monitor/registry: terminal-outcome record — a raced watch can recover the real down reason (soak sig 4)
A watch installed after its target's death has, until now, only NoProc to report — but the bridge's proxies install their native watch asynchronously after acquire returns, so a link established before a crash (from the BEAM's view) could still lose the panic's translated reason to that blanket NoProc (width-20 soak signature 4: link_test.exs:26, 1/600 full-suite, 3/2000 link-only, all whereis-miss; deterministic repro in the bridge suite). Two primitives, no change to monitor()'s own Erlang-faithful stale-pid semantics — the upgrade is the caller's deliberate act: - finalize_actor stamps the slot with (generation, DownReason) under the same cold-lock block that publishes the outcome. The record survives reclaim, registry pruning, and the next tenant's install; only the slot's next death overwrites it. terminal_reason(pid) reads it generation-matched. - resolve_name(name) is whereis with the corpse kept: the dead-holder arm returns the stored pid it prunes (NameResolution::Corpse) instead of discarding the only evidence of who died — whereis itself prunes on the way out, so a whereis-then-lookup consumer would find the evidence already destroyed. Live/Unbound match whereis's Some/None; the name heals exactly as before. Contract pinned in tests/terminal_outcome_after_death.rs: one record per way of dying (Exit/Panic/Stopped), no record while live, corpse capture + heal on resolve_name, record independence from registry pruning, survival across slot re-tenancy, overwrite at the next tenancy's death.
This commit is contained in:
@@ -486,6 +486,47 @@ pub fn whereis(name: &str) -> Option<Pid> {
|
||||
})
|
||||
}
|
||||
|
||||
/// What a name is bound to, three-valued (bridge soak signature 4).
|
||||
///
|
||||
/// [`Live`](NameResolution::Live) is [`whereis`]'s `Some`.
|
||||
/// [`Corpse`](NameResolution::Corpse) carries the *stored* holder pid of a
|
||||
/// dead-but-unpruned binding — a state Erlang cannot represent (its name
|
||||
/// death unregisters atomically; smarm's prune is lazy), captured here before
|
||||
/// the prune that `whereis` performs discards it, so the caller can consult
|
||||
/// [`terminal_reason`](crate::monitor::terminal_reason) for the tenancy's
|
||||
/// real down reason. [`Unbound`](NameResolution::Unbound) matches Erlang's
|
||||
/// unregistered name.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NameResolution {
|
||||
/// The stored holder is live (generation-checked); the binding stands.
|
||||
Live(Pid),
|
||||
/// The stored holder is dead. The binding was pruned on the way out —
|
||||
/// the name heals exactly as `whereis` heals it; only the evidence is
|
||||
/// returned instead of discarded. A second resolve is `Unbound`.
|
||||
Corpse(Pid),
|
||||
/// No binding stored (never registered, or already pruned by any reader).
|
||||
Unbound,
|
||||
}
|
||||
|
||||
/// Resolve `name` like [`whereis`], but keep the corpse: the dead-holder arm
|
||||
/// returns the stored pid it pruned instead of a bare `None`. Same lock
|
||||
/// discipline and pruning behavior as `whereis`; same `Runtime::run()`
|
||||
/// context contract.
|
||||
pub fn resolve_name(name: &str) -> NameResolution {
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
let Some(&pid) = reg.by_name.get(name) else {
|
||||
return NameResolution::Unbound;
|
||||
};
|
||||
if live(inner, pid) {
|
||||
NameResolution::Live(pid)
|
||||
} else {
|
||||
reg.prune_holder(pid);
|
||||
NameResolution::Corpse(pid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
|
||||
/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
|
||||
/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
|
||||
|
||||
Reference in New Issue
Block a user