feat(monitor): unidirectional one-shot process monitors
`monitor(pid) -> Receiver<Down>` lets any actor watch any other and
receive a single Down{pid, reason} when it terminates. Generalizes the
existing single supervisor_channel (which is just a hard-wired monitor
the parent installs at spawn).
- src/monitor.rs: Down / DownReason{Exit,Panic,NoProc} + monitor().
Payload-free: a panic payload has one owner and goes to the joiner via
JoinError, so monitors learn only that it panicked. Monitoring an
already-gone pid yields NoProc immediately.
- runtime: Slot grows `monitors: Vec<Sender<Down>>`; finalize_actor
drains and notifies them outside the shared lock (send can unpark a
receiver, which re-takes the non-reentrant mutex). Cleared in
reclaim_slot and on slot reuse at spawn.
- Registration and finalize both serialize on the shared mutex, so a
target alive at registration always delivers a real Down (no race
between liveness check and registration).
tests/monitor.rs: Exit, Panic, NoProc-on-dead-target, and fan-out to
multiple monitors. Full suite green.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
//! Process monitors.
|
||||
//!
|
||||
//! `monitor(target)` asks the runtime to deliver a single [`Down`] when
|
||||
//! `target` terminates, and hands back the [`Receiver`] to read it from. 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`.
|
||||
//!
|
||||
//! ## 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.
|
||||
|
||||
use crate::channel::{channel, Receiver};
|
||||
use crate::pid::Pid;
|
||||
use crate::runtime::State;
|
||||
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 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,
|
||||
}
|
||||
|
||||
/// Monitor `target`. Returns a channel that will receive 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 `recv()` returns without parking.
|
||||
pub fn monitor(target: Pid) -> Receiver<Down> {
|
||||
let (tx, rx) = channel::<Down>();
|
||||
|
||||
// Register under the shared lock. We clone the sender into the target's
|
||||
// monitor list and keep the original for the NoProc fallback; we must not
|
||||
// send while holding the lock, since `Sender::send` can call back into the
|
||||
// runtime (to unpark a parked receiver) and the shared mutex is not
|
||||
// reentrant.
|
||||
let registered = with_runtime(|inner| {
|
||||
inner.with_shared(|s| match s.slot_mut(target) {
|
||||
Some(slot) if !matches!(slot.state, State::Done) => {
|
||||
slot.monitors.push(tx.clone());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
});
|
||||
|
||||
if !registered {
|
||||
let _ = tx.send(Down { pid: target, reason: DownReason::NoProc });
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
Reference in New Issue
Block a user