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:
smarm-agent
2026-08-13 05:56:19 +00:00
parent b937f1f50f
commit 461fe4b768
4 changed files with 77 additions and 17 deletions
+34 -5
View File
@@ -58,6 +58,7 @@ impl GenServer for Filler {
#[derive(Debug)]
struct Observed {
exit_reason: Option<DownReason>,
anon_reason: Option<DownReason>,
panic_reason: Option<DownReason>,
stopped_reason: Option<DownReason>,
live_reason: Option<DownReason>,
@@ -83,25 +84,48 @@ fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
// Tiny slab: prompt slot recycling for the re-tenancy phase.
init(Config::exact(2).max_actors(32)).run(move || {
// --- Plain actors: one record per way of dying. -------------------
let h = smarm::spawn(|| {});
// --- Registered plain actors: one record per way of dying. The
// record is named-tenancy-only, so each actor self-registers a
// throwaway channel before dying; the anonymous control below pins
// the complement.
let h = smarm::spawn(|| {
let (tx, _rx) = smarm::channel::<()>();
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_exit"), tx);
});
let pid_exit = h.pid();
let _ = h.join();
let exit_reason = terminal_reason(pid_exit);
let h = smarm::spawn(|| panic!("induced"));
let h = smarm::spawn(|| {
let (tx, _rx) = smarm::channel::<()>();
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_panic"), tx);
panic!("induced");
});
let pid_panic = h.pid();
let _ = h.join();
let panic_reason = terminal_reason(pid_panic);
let h = smarm::spawn(|| loop {
smarm::sleep(Duration::from_millis(2));
let h = smarm::spawn(|| {
let (tx, _rx) = smarm::channel::<()>();
let _ = smarm::register(smarm::Name::<()>::new("terminal_probe_stop"), tx);
loop {
smarm::sleep(Duration::from_millis(2));
}
});
let pid_stop = h.pid();
request_stop(pid_stop);
let _ = h.join();
let stopped_reason = terminal_reason(pid_stop);
// --- Anonymous control: an unregistered death must NOT stamp (nor
// evict) — the free list is LIFO, so green-thread churn would
// otherwise overwrite a watchable record faster than any race
// window this exists to cover.
let h = smarm::spawn(|| panic!("anonymous"));
let pid_anon = h.pid();
let _ = h.join();
let anon_reason = terminal_reason(pid_anon);
// --- The named target: live readings first. -----------------------
let target = GenServerBuilder::new(Target)
.named(TARGET)
@@ -165,6 +189,7 @@ fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
*out_w.lock().unwrap() = Some(Observed {
exit_reason,
anon_reason,
panic_reason,
stopped_reason,
live_reason,
@@ -181,6 +206,10 @@ fn terminal_record_recovers_the_reason_a_raced_watch_lost() {
let o = out.lock().unwrap().take().expect("runtime body completed");
assert_eq!(o.exit_reason, Some(DownReason::Exit), "{o:?}");
assert_eq!(
o.anon_reason, None,
"anonymous deaths must not stamp: {o:?}"
);
assert_eq!(o.panic_reason, Some(DownReason::Panic), "{o:?}");
assert_eq!(o.stopped_reason, Some(DownReason::Stopped), "{o:?}");
assert_eq!(