From 70bd23727799cc81ea1d65c846f9ff1c405ab1b6 Mon Sep 17 00:00:00 2001 From: smarm-dev Date: Fri, 5 Jun 2026 21:17:19 +0000 Subject: [PATCH] docs: mark links/trap_exit (#3) done; refresh supervisor + README - task.md: mark roadmap #3 done (record the resolved dedicated-inbox decision + test list), add the feature commit to the resume block, note the now-resolved trap_exit channel question, and list spawn_link under #5. - supervisor.rs: the module comment predated cancellation; rewrite it to say sibling/link/shutdown stops are cooperative (request_stop), not that one_for_all / rest_for_one / links are impossible. - README: add monitor + link rows, refresh the supervisor row and src layout, and drop restart-intensity caps from "what's not here" (shipped in #2). --- README.md | 9 ++++++--- src/supervisor.rs | 17 ++++++++++------- task.md | 44 ++++++++++++++++++++++++++++---------------- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 92733dc..3b14de9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ convenience wrapper around `runtime::init(Config::exact(1)).run(f)`. | `mutex` | `Mutex` with mandatory timeout; FIFO waiters; parks the green thread | | `timer` | Min-heap of `(deadline, reason)`; `Sleep` and `WaitTimeout` reasons | | `io` | `block_on_io` for blocking work; `wait_readable`/`wait_writable` + `read`/`write` via epoll | -| `supervisor` | `Signal::Exit` / `Signal::Panic` delivered to a parent actor's mailbox | +| `supervisor` | `Signal::Exit`/`Panic`/`Stopped` funnelled to a parent; `OneForOne`/`OneForAll`/`RestForOne` strategies + restart-intensity cap | +| `monitor` | `monitor(pid)` → one-shot `Receiver`; unidirectional death notice | +| `link` | bidirectional `link`/`unlink`; abnormal death propagates (cooperative stop, or an `ExitSignal` message under `trap_exit`) | ## Quick taste @@ -52,7 +54,8 @@ run(|| { ``` src/ stack.rs context.rs preempt.rs pid.rs actor.rs - scheduler.rs channel.rs mutex.rs timer.rs io.rs supervisor.rs + scheduler.rs channel.rs mutex.rs timer.rs io.rs + supervisor.rs monitor.rs link.rs runtime.rs lib.rs tests/ per-module integration tests @@ -76,7 +79,7 @@ cargo bench # primes benchmark vs tokio ## What's not here See the **Defer** section of `Architecture.md`. -restart-intensity caps, `join!` for handle groups, stack growth via remap, +`join!` for handle groups, stack growth via remap, hierarchical timer wheel, fd-wait timeouts, `Signal::Timeout`. Each is mechanism we know how to add; none belongs in this iteration. diff --git a/src/supervisor.rs b/src/supervisor.rs index 99f5507..41ca911 100644 --- a/src/supervisor.rs +++ b/src/supervisor.rs @@ -51,13 +51,16 @@ impl Signal { // whether to restart, and enforce a restart-intensity cap so a child that // crashes in a tight loop eventually gives up instead of spinning forever. // -// What this deliberately does NOT do: forcibly terminate a running child. -// smarm actors share a heap and rely on Drop/RAII, so async teardown of a -// peer's stack from outside is unsound. That rules out `one_for_all` / -// `rest_for_one` (which must stop siblings) and true bidirectional links -// until there is a cooperative-cancellation primitive. When the intensity cap -// trips, this supervisor simply stops restarting and returns; it does not kill -// whatever children are still alive. +// What this does NOT do: *forcibly* terminate a running child. smarm actors +// share a heap and rely on Drop/RAII, so tearing down a peer's stack from +// outside is unsound. Stopping a sibling — whether for `one_for_all` / +// `rest_for_one`, for a propagated link death, or for the ordered shutdown +// below — is therefore *cooperative*: `request_stop` flags the child and it +// unwinds at its next observation point. A child with no observation points +// (a tight loop with no `check!()`, allocation, or blocking op) cannot be +// stopped, exactly as it cannot be preempted. When the intensity cap trips, +// this supervisor stops restarting and tears the remaining children down in +// reverse start order before returning. // --------------------------------------------------------------------------- use crate::channel::channel; diff --git a/task.md b/task.md index 55d1394..54ac0a7 100644 --- a/task.md +++ b/task.md @@ -5,7 +5,7 @@ before starting; the gotchas section is hard-won and will save you a faceplant. ## Resume the environment -- Repo: `/home/claude/work/smarm`, branch `arm-port`, currently 6 commits past +- Repo: `/home/claude/work/smarm`, branch `arm-port`, currently 8 commits past `master`: - `a8fec02` aarch64 context-switch port - `183fd05` monitors (`monitor()` → `Receiver`) @@ -13,6 +13,9 @@ before starting; the gotchas section is hard-won and will save you a faceplant. - `a8ddb4a` cooperative cancellation (roadmap #1 — done) - `e80334b` fix: orphaned timers no longer block runtime shutdown - `351dc9c` one_for_all / rest_for_one + ordered shutdown (roadmap #2 — done) + - `31133a6` docs(roadmap): mark #1 and #2 done + - `6581484` links + trap_exit (roadmap #3 — done) + - (+ a trailing docs commit marking #3 done and refreshing supervisor/README) - Toolchain is installed but NOT on PATH in a fresh shell. First line of every session: `. "$HOME/.cargo/env"` (rustc/cargo 1.96). - Build `cargo build` · all tests `cargo test` · one suite `cargo test --test monitor`. @@ -101,19 +104,24 @@ Shipped. What landed vs the plan: rather than folding into Exit (clearer for the supervisor's await logic). - Tests: all-restart, suffix-restart, reverse-order shutdown. -### 3. Links + trap_exit ← NEXT -- `Slot.links: Vec` (bidirectional); `link`/`unlink`; per-actor - `trap_exit` flag. -- On finalize, for each linked peer: abnormal death → `request_stop(peer)` - unless peer traps, in which case deliver an exit *message* instead. Normal - exit does NOT propagate. Linking an already-dead pid delivers an immediate - exit signal. -- Open decision: which channel a trapping actor reads exit messages from - (reuse a Down-style channel vs a dedicated trap inbox) — ties into the - no-unified-mailbox constraint below. (`Signal::Stopped` + the reverse-order - stop helper from #2 are now available to build on.) -- Tests: linked pair one panics → other stopped; with trap_exit → other gets a - message and survives; normal exit doesn't propagate. +### 3. Links + trap_exit ✅ DONE (`6581484`) +- `Slot.links: Vec` (bidirectional); `link`/`unlink`; `trap_exit()` flag + lives on `Actor` (fresh per spawn → a restarted child starts un-trapped, and + no fourth slot-reset site). +- On finalize, reverse links are cleared under the lock (always — keeps the + cascade acyclic), then for each linked peer: abnormal death (`Panic`/ + `Stopped`) → `request_stop(peer)` unless peer traps, in which case deliver an + `ExitSignal` *message* instead. Normal exit does NOT propagate. Linking an + already-dead pid delivers an immediate `NoProc` signal (message if trapping, + else `request_stop(self)` — not a silent no-op). +- Resolved: the trap inbox is a **dedicated** channel (`trap_exit() -> + Receiver`), distinct from the monitor `Down` channel; `ExitSignal` + reuses `DownReason` and carries no panic payload (joiner-only, as with + monitors). `spawn_link` deferred to #5. +- Tests (`tests/link.rs`): linked pair one panics → other stopped (+Drop ran); + trap_exit → other gets a message and survives; normal exit doesn't propagate; + dead-pid link stops a non-trapper / messages a trapper; `unlink` prevents + propagation. ### 4. Selective receive (independent track) Channels are strict FIFO MPSC; there's no pattern-matched `receive`. @@ -126,6 +134,9 @@ Channels are strict FIFO MPSC; there's no pattern-matched `receive`. remain for a later `recv`. ### 5. Grab-bag (each its own small commit) +- `spawn_link`: spawn-and-link atomically (deferred from #3); thin wrapper over + `spawn_under` + `link`, but do it under one lock so there's no window where + the child dies before the link is recorded. - `demonitor`: needs a per-monitor id to remove a specific sender. Decide the monitor API NOW before more code depends on it — likely return a `Monitor { id, rx }` instead of a bare `Receiver`. @@ -153,8 +164,9 @@ Channels are strict FIFO MPSC; there's no pattern-matched `receive`. - **Pid = (index, generation);** stale handles caught by generation mismatch in `slot()/slot_mut()`. The monitor `NoProc` path relies on this. - **No `select`, no unified per-process mailbox.** Why the supervisor uses the - single `supervisor_channel` funnel rather than N monitor channels, and why - trap_exit/selective-receive delivery need an explicit channel decision. + single `supervisor_channel` funnel rather than N monitor channels. trap_exit + resolved this by giving each trapping actor a dedicated `Receiver` + inbox (see #3); selective-receive delivery still needs its own decision. - **Cooperative-only**: preemption and (future) cancellation both depend on the actor reaching `check!()`/yield/alloc/blocking points. - `run()` is single-thread (`Config::exact(1)`); tests rely on deterministic