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:
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user