diff --git a/src/monitor.rs b/src/monitor.rs
index ce4bdc8..4e7ecd1 100644
--- a/src/monitor.rs
+++ b/src/monitor.rs
@@ -1,49 +1,85 @@
-//! Process monitors.
+//! Find out when another actor dies, without it knowing or caring that you're
+//! watching.
//!
-//! `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:
+//! Say one actor manages a pool of workers and needs to know when a worker
+//! exits, so it can replace it. The worker does not need to know it is being
+//! watched, and nothing about the worker's own behavior should change because
+//! someone is watching it. That is what [`monitor`] is for: call
+//! `monitor(target)` to get a [`Monitor`], and read exactly one [`Down`]
+//! message off `monitor.rx` whenever `target` terminates, however it
+//! terminates.
//!
-//! - **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)`.
+//! ```
+//! use smarm::{monitor, run, spawn, DownReason};
//!
-//! 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.
+//! run(|| {
+//! let worker = spawn(|| {
+//! // does some work, then returns
+//! });
+//! let pid = worker.pid();
//!
-//! ## Reasons
+//! let m = monitor(pid);
+//! let _ = worker.join();
//!
-//! [`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`.
+//! let down = m.rx.recv().expect("monitor channel closed before Down");
+//! assert_eq!(down.pid, pid);
+//! assert_eq!(down.reason, DownReason::Exit);
+//! });
+//! ```
//!
-//! ## Demonitoring
+//! A monitor is one-directional and one-shot:
//!
-//! 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])`.
+//! - **One-directional**: the watcher learns that the target died, but the
+//! target is completely unaffected. It never learns it was being watched,
+//! and its own behavior and lifetime do not change because of the monitor.
+//! This is the opposite of a [`link`](mod@crate::link), which is bidirectional:
+//! linking two actors means an abnormal death on either side can bring the
+//! other down too. Reach for a monitor when you just want to *know*; reach
+//! for a link when a peer's crash should actually stop you.
+//! - **One-shot**: you get exactly one [`Down`] per `monitor()` call, then the
+//! channel closes. Calling `monitor` again on the same target (or a
+//! different one) gives you an independent registration with its own
+//! [`Monitor`] and its own one-shot channel; nothing stops you from
+//! monitoring the same actor many times over; each call is watched and
+//! fires on its own.
//!
-//! ## Races
+//! ## Why a monitor never hands you the panic value
//!
-//! 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.
+//! If the target panicked, [`Down`] tells you *that* it panicked
+//! ([`DownReason::Panic`]), but not the panic's payload. The payload has a
+//! single owner: it is handed to whichever caller `join()`s the actor's
+//! [`JoinHandle`](crate::JoinHandle), as a `JoinError`. A monitor only needs
+//! to know that something went wrong, not reproduce the exact value that
+//! caused it, so it gets the reason and nothing else.
+//!
+//! Monitoring a target that is already gone (it finished and was cleaned up,
+//! or the pid never pointed at a real actor) is not an error: you get a
+//! [`Down`] with [`DownReason::NoProc`] right away, instead of waiting
+//! forever for something that already happened.
+//!
+//! ## Stopping a monitor early
+//!
+//! [`demonitor`] cancels a monitor before it fires. If the registration was
+//! still live, it removes it and returns `Some` of the monitor's id: no
+//! `Down` will arrive on that channel from here on. If the target had already
+//! died and its `Down` already sent, there is nothing left to cancel and
+//! `demonitor` returns `None`; the `Down` you already have (or that is
+//! already sitting in the channel) is unaffected.
+//!
+//! If you want to cancel *and* make sure a `Down` that already arrived is
+//! discarded without reading it, just drop the [`Monitor`]: dropping it closes
+//! its receiver, and any queued `Down` is dropped along with it.
+//!
+//! ## Correctness notes for implementers
+//!
+//! A target that is still alive at the moment `monitor()` registers is
+//! guaranteed to eventually produce a real `Down`: registration and the
+//! target's own termination bookkeeping run under the same lock, so there is
+//! no window in which the target could die without the just-added
+//! registration seeing it. `demonitor` is similarly race-free against a target
+//! that has since died and had its slot reused by a new, unrelated actor: it
+//! is checked against the exact monitored incarnation, so it can never remove
+//! a different actor's registration by accident, it simply reports `None`.
use crate::channel::{channel, Receiver, Sender};
use crate::pid::Pid;
@@ -51,8 +87,8 @@ 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.
+/// Carries no payload: see the module docs for why a monitor never receives
+/// the panic value itself.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DownReason {
/// The target returned normally.
@@ -76,21 +112,22 @@ pub struct Down {
pub reason: DownReason,
}
-/// A process-unique identifier for one `monitor()` registration.
+/// A 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.
+/// Opaque and `Copy`. Never reused for the life of the runtime, so if you
+/// monitor the same target more than once, each call's id is distinct. This
+/// is what lets [`demonitor`] tear down exactly one of several monitors on
+/// the same target without disturbing the others.
#[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.
+/// Read the notification from [`Monitor::rx`]. Not `Clone`, since only one
+/// side is meant to consume it. Dropping a `Monitor` closes the receiving
+/// end; if a `Down` had already arrived but was never read, it is discarded
+/// along with it.
pub struct Monitor {
/// This registration's process-unique id.
pub id: MonitorId,
@@ -110,10 +147,11 @@ pub fn monitor(target: Pid) -> Monitor {
let target = target.erase();
let (tx, rx) = channel::();
- // 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,
+ // Implementation note: registration happens under the target's cold
+ // lock. `tx.clone()` takes the channel's own lock, a Channel-class
+ // RawMutex, which is explicitly permitted under a Leaf (cold) lock by
+ // the lock order documented in raw_mutex.rs. We must still not *send*
+ // under the lock, since `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();
@@ -140,19 +178,21 @@ pub fn monitor(target: Pid) -> Monitor {
}
/// 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.
+/// and removed, so no `Down` will arrive on `m.rx` from here on. Returns
+/// `None` if there was nothing left to remove: the target had already gone
+/// down and its `Down` was already sent (or is already sitting in the
+/// channel, unread).
///
-/// 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])`.
+/// This only stops a *future* `Down`. If you also want to discard a `Down`
+/// that already arrived (or is about to, in a race with this call), drop `m`
+/// instead of, or in addition to, calling this: dropping the [`Monitor`]
+/// closes its receiver and any queued notice is discarded with it.
pub fn demonitor(m: &Monitor) -> Option {
- // 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.
+ // Implementation note: the registration is removed under the target's
+ // cold lock, but the `Sender` is moved *out* and dropped 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)> = with_runtime(|inner| {
let slot = inner.slot_at(m.target)?;
let mut cold = slot.cold.lock();