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:
@@ -472,6 +472,14 @@ pub(crate) struct SlotCold {
|
||||
/// epoch-matched unpark.
|
||||
pub(crate) waiters: Vec<(Pid, u32)>,
|
||||
pub(crate) outcome: Option<Outcome>,
|
||||
/// The slot's most recent *death*: `(generation, reason)`, stamped by
|
||||
/// `finalize_actor` and deliberately never cleared — a new tenant's
|
||||
/// install leaves it standing (it describes the previous tenancy), and
|
||||
/// only the next death overwrites it. Read generation-matched via
|
||||
/// [`terminal_reason`](crate::monitor::terminal_reason), so a watch that
|
||||
/// raced its target's death can recover the real down reason instead of
|
||||
/// a blanket `NoProc` (bridge soak signature 4).
|
||||
pub(crate) terminal: Option<(u32, DownReason)>,
|
||||
pub(crate) supervisor_channel: Option<Sender<Signal>>,
|
||||
/// Watchers registered via `monitor()`, each tagged with its
|
||||
/// `MonitorId` so `demonitor` can remove exactly one. Each receives one
|
||||
@@ -626,6 +634,7 @@ impl Slot {
|
||||
actor: None,
|
||||
waiters: Vec::new(),
|
||||
outcome: None,
|
||||
terminal: None,
|
||||
supervisor_channel: None,
|
||||
monitors: Vec::new(),
|
||||
links: Vec::new(),
|
||||
@@ -1667,6 +1676,11 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
||||
None => panic!("finalize_actor: actor vanished"),
|
||||
};
|
||||
cold.outcome = Some(joiner_outcome);
|
||||
// Terminal record (soak sig 4): stamped before the generation ever
|
||||
// bumps, under the cold lock, so a reader that resolved this pid can
|
||||
// recover the reason after the slot moves on. Overwritten only by the
|
||||
// slot's next death.
|
||||
cold.terminal = Some((pid.generation(), down_reason));
|
||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||
// Done is published under the cold lock, so join's
|
||||
// check-Done-or-register-waiter (also under it) can never miss: it
|
||||
|
||||
Reference in New Issue
Block a user