Files
smarm/src/monitor.rs
T
smarm 8ff6cf4afd feat(channel): migrate Inner to RawMutex; two-class lock order (Leaf -> Channel)
Phase 2 surfaced the hole this closes: channel guards were held with
preemption enabled, so a timeslice switch inside a channel critical
section could release the pthread mutex from a different OS thread (UB).
RawMutex disables preemption for the guard span and is
cross-thread-release sound by construction; poison goes away with it.

The strict single-class leaf rule cannot survive the migration: finalize
clones the supervisor/trap senders and monitor() clones the Down sender,
all under a cold lock, and those senders live in the slot - the nesting
is structural. The leaf check is therefore generalized to two classes:
Leaf (cold locks, free list, stack pool) and Channel. Order is
Leaf -> Channel, at most one of each; both directions of violation are
debug-asserted at the acquisition site. Channel critical sections call
only the lock-free unpark protocol, so the order is acyclic.

Tests: class-ordering unit tests in raw_mutex (allowed nesting + all
three rejected shapes), plus a multi-scheduler integration test driving
channels through monitor churn and actor death so any ordering
regression trips the debug assert instead of deadlocking.
2026-06-09 22:51:37 +00:00

167 lines
7.1 KiB
Rust

//! Process monitors.
//!
//! `monitor(target)` asks the runtime to deliver a single [`Down`] when
//! `target` terminates, and hands back a [`Monitor`] — the [`Receiver`] to read
//! it from, plus the identity (`id`, `target`) needed to take the registration
//! back down with [`demonitor`]. A monitor is:
//!
//! - **unidirectional** — the watcher learns of the target's death, but the
//! target learns nothing of the watcher, and the watcher is unaffected by
//! the death beyond the notification (contrast a *link*, which propagates
//! failure);
//! - **one-shot** — exactly one `Down` is ever sent for a given monitor.
//! The returned channel closes afterwards, so a second `recv()` yields
//! `Err(RecvError)`.
//!
//! This generalizes the older single-`supervisor_channel` mechanism: a
//! supervisor is just a hard-wired monitor that the parent installs at spawn
//! time. Here any actor may monitor any pid, any number of times.
//!
//! ## Reasons
//!
//! [`DownReason`] is deliberately payload-free. A panicking actor's payload
//! has a single owner and is delivered to whoever `join()`s the actor (as
//! `JoinError`); a monitor only learns *that* it panicked, not the value.
//! Monitoring a pid that is already gone (reclaimed, or never alive) yields
//! [`DownReason::NoProc`] immediately, mirroring Erlang's `noproc`.
//!
//! ## Demonitoring
//!
//! Each `monitor()` registration is tagged with a process-unique [`MonitorId`].
//! [`demonitor`] removes the registration named by a [`Monitor`] from its
//! target's slot, returning `Some(id)` if a live registration was found or
//! `None` if it had already fired (or the target is gone). Dropping the
//! [`Monitor`] afterwards discards any `Down` that the target had *already*
//! queued — the equivalent of Erlang's `demonitor(Ref, [flush])`.
//!
//! ## Races
//!
//! Registration (below) and `finalize_actor` (in `runtime`) both run under the
//! shared-state mutex, so a target that is still alive when its monitor is
//! registered is guaranteed to deliver a real `Down`; there is no window in
//! which the death slips between the liveness check and the registration.
//! `demonitor` is protected by the generation half of the pid: if the target
//! has died and its slot index been recycled, `slot_mut(target)` fails the
//! generation check and `demonitor` is a clean no-op — it can never strip a
//! *different* actor's monitor that happens to share the slot index.
use crate::channel::{channel, Receiver, Sender};
use crate::pid::Pid;
use crate::scheduler::with_runtime;
/// Why a monitored actor went down.
///
/// `Copy` because it carries no payload — see the module docs for why the
/// panic payload is *not* included here.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DownReason {
/// The target returned normally.
Exit,
/// The target panicked. The payload is delivered to the actor's joiner,
/// not to monitors.
Panic,
/// The target was cooperatively cancelled via `request_stop`.
Stopped,
/// The target was already gone (finished and reclaimed, or never alive)
/// at the moment `monitor()` was called.
NoProc,
}
/// A monitored actor's termination notice.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Down {
/// The pid that was being monitored.
pub pid: Pid,
/// How it went down.
pub reason: DownReason,
}
/// A process-unique identifier for one `monitor()` registration.
///
/// Opaque and `Copy`. Allocated from a monotonic counter in shared state, so
/// it is never reused for the lifetime of the runtime — distinct `monitor()`
/// calls on the same target get distinct ids, which is what lets [`demonitor`]
/// tear down exactly one of several monitors on a target.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MonitorId(pub(crate) u64);
/// A live monitor: the receiving end of the one-shot [`Down`] channel, plus the
/// identity needed to [`demonitor`] it.
///
/// Read the notification from [`Monitor::rx`]. Not `Clone` (the receiver is a
/// single consumer). Dropping it closes the receiving end; if a `Down` was
/// already queued it is discarded with the channel.
pub struct Monitor {
/// This registration's process-unique id.
pub id: MonitorId,
/// The pid being monitored.
pub target: Pid,
/// The one-shot channel the `Down` arrives on.
pub rx: Receiver<Down>,
}
/// Monitor `target`. Returns a [`Monitor`] whose `rx` receives exactly one
/// [`Down`].
///
/// If `target` is still live, the `Down` arrives when it terminates. If
/// `target` is already gone, a [`DownReason::NoProc`] `Down` is queued
/// immediately so the caller's `rx.recv()` returns without parking.
pub fn monitor(target: Pid) -> Monitor {
let (tx, rx) = channel::<Down>();
// Register under the target's cold lock. `tx.clone()` takes the channel's
// own lock — a Channel-class RawMutex, explicitly permitted *under* a Leaf
// (cold) lock by the lock order (see raw_mutex.rs). We must still not
// *send* under the lock, as `Sender::send` can unpark a parked receiver,
// and there's no reason to nest that.
let (id, registered) = with_runtime(|inner| {
let id = inner.alloc_monitor_id();
let registered = match inner.slot_at(target) {
Some(slot) => {
let mut cold = slot.cold.lock();
if slot.is_live_for(target) {
cold.monitors.push((id, tx.clone()));
true
} else {
false
}
}
None => false,
};
(id, registered)
});
if !registered {
let _ = tx.send(Down { pid: target, reason: DownReason::NoProc });
}
Monitor { id, target, rx }
}
/// Cancel the monitor `m`. Returns `Some(id)` if a live registration was found
/// on the target's slot and removed, or `None` if there was nothing to remove
/// — the target already fired its `Down` (the registration is drained on
/// finalize), was never alive (`NoProc`), or has been reclaimed.
///
/// This stops any *future* `Down`. To also discard a `Down` the target may have
/// *already* queued (the finalize-races-demonitor case), drop `m` afterwards;
/// dropping the [`Monitor`] closes its receiver and the queued notice goes with
/// it — the analogue of Erlang's `demonitor(Ref, [flush])`.
pub fn demonitor(m: &Monitor) -> Option<MonitorId> {
// Remove the registration under the target's cold lock, but move the
// `Sender` *out* and let it drop only after the lock is released:
// dropping the last sender runs `Sender::drop`, which may unpark a parked
// receiver — legal under a cold lock, but pointless to nest.
let removed: Option<(MonitorId, Sender<Down>)> = with_runtime(|inner| {
let slot = inner.slot_at(m.target)?;
let mut cold = slot.cold.lock();
if slot.generation() != m.target.generation() {
return None; // slot reused; the Down already fired
}
let pos = cold.monitors.iter().position(|(mid, _)| *mid == m.id)?;
Some(cold.monitors.remove(pos))
});
// `removed`'s sender drops here, outside the lock.
removed.map(|(id, _sender)| id)
}