fix(runtime): only named tenancies stamp the terminal record — anonymous churn must not evict it
Discovered wiring the bridge consult: with an unconditional stamp, the record for the very death being raced was the shortest-lived data in the runtime. Every green thread is a slot tenant, the free list is LIFO — so the slot a named server's death frees is the first one recycled, and the next throwaway exit (monitor holders, chain-runner work, anything) overwrote the record before a raced watch could consult it. Deterministic bridge repro: the corpse resolved fine, terminal_reason read None every time. register_with now flags the tenancy (ever_named, reset at reclaim) before the binding lands — set outside the registry lock, so no successfully registered actor can die unflagged and a failed register's overshoot is harmless — and finalize stamps only flagged tenancies. Watchable identities are exactly the named ones (the bridge's pid-identity path deliberately keeps Erlang's raw :noproc), so nothing consultable is lost. Contract test updated: the three death modes now self-register; a new anonymous control pins that unregistered deaths neither stamp nor evict.
This commit is contained in:
+26
-7
@@ -472,14 +472,24 @@ 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
|
||||
/// The slot's most recent *named-tenancy* death: `(generation, reason)`,
|
||||
/// stamped by `finalize_actor` — but only for a tenancy that ever
|
||||
/// registered a name (`ever_named`) — and deliberately never cleared: a
|
||||
/// new tenant's install leaves it standing (it describes the previous
|
||||
/// tenancy), and only the next *named* death overwrites it. Anonymous
|
||||
/// green-thread churn must not evict it: the free list is LIFO, so the
|
||||
/// just-freed slot is the first recycled, and an unconditional stamp
|
||||
/// made a watchable tenancy's record the shortest-lived data in the
|
||||
/// runtime. 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)>,
|
||||
/// This tenancy registered a name at least once — the stamp-eligibility
|
||||
/// bit for `terminal` above. Set by `register_with` *before* the binding
|
||||
/// lands (so no successfully-registered actor can die unflagged; a
|
||||
/// failed register's overshoot is harmless), reset at reclaim.
|
||||
pub(crate) ever_named: bool,
|
||||
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
|
||||
@@ -635,6 +645,7 @@ impl Slot {
|
||||
waiters: Vec::new(),
|
||||
outcome: None,
|
||||
terminal: None,
|
||||
ever_named: false,
|
||||
supervisor_channel: None,
|
||||
monitors: Vec::new(),
|
||||
links: Vec::new(),
|
||||
@@ -1637,6 +1648,7 @@ pub(crate) fn reclaim_slot(inner: &RuntimeInner, pid: Pid) {
|
||||
cold.waiters.clear();
|
||||
cold.monitors.clear();
|
||||
cold.links.clear();
|
||||
cold.ever_named = false;
|
||||
slot.reset_counters();
|
||||
slot.stop_ptr.store(std::ptr::null_mut(), Ordering::Release);
|
||||
// The generation bump IS the reclaim: every stale pid is dead from
|
||||
@@ -1678,9 +1690,16 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
||||
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));
|
||||
// recover the reason after the slot moves on — but only for a
|
||||
// tenancy that ever held a name. The free list is LIFO, so the slot
|
||||
// this death frees is the very next one recycled; if every green
|
||||
// thread's exit stamped too, the churn behind any real workload
|
||||
// would evict a watchable tenancy's record in well under the race
|
||||
// window this exists to cover. Overwritten only by the slot's next
|
||||
// *named* death.
|
||||
if cold.ever_named {
|
||||
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