feat(link): bidirectional links + trap_exit (roadmap #3)

Add Erlang-style process links so an abnormal death fate-shares across a
link set, with trap_exit to convert that into a message instead.

- Slot.links: Vec<Pid>, bidirectional; reset in all three lifecycle sites.
- Actor.trap: Option<Sender<ExitSignal>>, fresh per spawn (a restarted
  child starts un-trapped; no fourth reset site).
- link/unlink free functions on self_pid(); trap_exit() -> Receiver<ExitSignal>
  (a dedicated inbox, distinct from the monitor Down channel).
- finalize_actor clears reverse links under the lock (always; keeps the
  cascade acyclic), then propagates abnormal deaths post-lock: trapping peer
  gets an ExitSignal message and survives, non-trapping peer is request_stop'd.
  Normal exit never propagates. Dead-pid link delivers an immediate NoProc
  (message if trapping, else request_stop(self) -- never a silent no-op).
- ExitSignal reuses DownReason and carries no panic payload (joiner-only).

Tests in tests/link.rs cover the propagate/trap/normal/dead-pid/unlink cases.
This commit is contained in:
smarm-dev
2026-06-07 21:51:56 +00:00
parent 59998ce79e
commit 8ac11a57ac
6 changed files with 443 additions and 2 deletions
+7
View File
@@ -134,4 +134,11 @@ pub struct Actor {
/// (constructed fresh at spawn), so unlike a `Slot` field it needs no reset
/// in the slot-recycling paths.
pub stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// Trap-exit inbox (roadmap #3). `None` until the actor calls
/// `link::trap_exit()`; `Some(tx)` means an abnormal death of a linked
/// peer is delivered here as an `ExitSignal` *message* instead of
/// cooperatively stopping this actor. Lives on the `Actor` (fresh per
/// spawn) for the same reason as `stop`: no slot-recycling reset needed,
/// and a restarted child correctly starts out *not* trapping.
pub trap: Option<crate::channel::Sender<crate::link::ExitSignal>>,
}