Root cause behind the "pin the endpoint" gotcha and the trapping-wrapper
pattern: a gen_server had two lifetime authorities — its refs (last one
dropped → inbox closes → exit) and, when supervised, its supervisor. OTP has
one: a process lives until it stops, is shut down, or is killed; a pid is an
address. Root exit now shutting down every forest root removes the reason the
ref-governed idiom existed (a forgotten server no longer hangs the run), so
adopt the one rule:
- The server/machine loop holds one inbox sender for its life; the inbox
never closes. GenServerRef / GenStatemRef are addresses. Explicit close is
`shutdown()`; a forgotten one is swept at root exit.
- `NamedGenServerBuilder::run()` / `gen_statem::run_named(name, m)` run the
loop inline as the current actor: a server is a direct ChildSpec child,
gets the supervisor's shutdown as handle_shutdown / a shutdown row, re-binds
its name on restart, and is addressed by name. The wrapper in
examples/graceful_shutdown.rs is gone.
- gen_statem gains GenStatemName + whereis_machine/send/call/shutdown by name
(parity with gen_server); the macro gets `Sm::new`.
- Root-exit sweep records `Event::RootSweep { target, trapping }` under
smarm-trace ("root_sweep shutdown|stopped"): unsupervised leftovers are
visible rather than silently owned-by-refs.
- Named start() name-clash path stops the spawned actor instead of relying
on ref drop.
Tests: tests/gen_server_lifetime.rs, tests/gen_statem_lifetime.rs,
tests/root_sweep_trace.rs (feature-gated); three existing tests that used
drop-closes-inbox now use shutdown(). Docs/README/ROADMAP/Deep Dive updated.
The RFC 014 root-exit sweep hard-stopped every live slot once nothing was
runnable. That deferral privileged queued work over parked-with-a-pending-
wake work (a sleeper was killed, a queued cast was drained) and any attempt
to widen the notion of pending wake (timers, fd readiness) re-wedges the
run on the periodic-timer daemon the sweep exists to end.
Root exit now means "the program is done": finalize_actor delivers
request_shutdown to every forest root — each live actor whose parent is
the run (ROOT_PID) or is dead — synchronously, before the live-count
decrement. Supervisors cascade per child Shutdown policy; trapping actors
may Continue/drain with working timers and end the run when they stop
themselves; non-trapping actors are stopped outright. No forcing sweep.
Removes root_exited/root_swept, Pop::RootDrain and the idle-verdict
condition; adds tests/root_exit.rs.
Add a cancellable message-delivery timer on the existing timer.rs min-heap,
the substrate the gen_server time idioms (idle/receive timeout, periodic tick,
debounce/backoff) need.
- Reason::Send { fire }: a type-erased delivery thunk. send_after (Pid<A>, via
send_to) and send_after_named (Name<M>, via send) capture dest+msg and
resolve the address on fire, not at arm time, so a dead target / restarted
name is observed when it fires; a failed resolve or closed inbox is dropped
(Erlang erlang:send_after semantics).
- Cancellation via an "armed" set keyed on the entry seq, exposed as an opaque
TimerId. Only Send timers use it; Sleep/WaitTimeout keep their inert-stale
behaviour untouched. pop_due fires a Send only while still armed and removes
it, so cancel returns true iff it landed before the fire (the race signal).
cancel is unscoped: any holder of the id can cancel (e.g. racing two servers
and cancelling the slow path).
- peek_deadline contract documented as "<= true next deadline" so a future
hierarchical timing wheel can back Timers without touching send_after or the
scheduler idle path. call_timeout left on recv_timeout (blast radius).
Tests: Timers-level (fire/cancel/race/clear/ordering) plus scheduler-level
delivery for both Name<M> and Pid<A>, cancel-prevents-delivery, and silent
drop on unresolved name / dead pid. Green on rq-mutex/rq-mpmc/rq-striped.
Move typed addressable mailboxes (RFC 013 directory rework + RFC 014 typed
addressing/producers/naming/root-exit teardown) into Shipped, with the final
phase's delivered surface enumerated. Retarget Up next to the gen_server
time-related patterns: send_after/cancel_timer substrate + the OTP time idioms
(idle/receive timeout, periodic tick/heartbeat, debounce/backoff) on the v0.8
server loop. Fold the duplicate Later send_after bullet into Up next, and mark
the "app actor blocks AllDone" Look-into as addressed by the root-exit teardown.
- add RFC 013 as the up-next item, sequenced before send_after
- mark process groups shipped (RFC 012, b78311b..56f2fc5)
- note send_after now depends on 013 for Dest; cross-link clustering name-addressing
- Compact v0.9 (Wake-path latency) into Shipped; record the RFC 004 spinning
excision as a deviation (preserved on branch rfc-004-spinning).
- Drop the v0.7 entry, folding its reference into the Shipped history pointer.
- Promote Process groups from Later into the up-next slot v0.9 vacated;
Per-switch cost now leads Later/Highest priority.
The spinning experiment is excised from master. Drops the two dedicated v0.9
sections describing it. Inline RFC-004 mentions in the v0.9 intro and the
queue-topology decision record are intentionally left for a manual short-term
-plan pass.
The terminal-wake fix cannot reach the (deadline, no-io) idle branch:
thread::sleep, no pipe to write. Documented as a Later item with the
two candidate mechanisms (clamped sleep vs condvar/futex), plus the
cross-thread-unpark observation from the same session.
Shootout (6d9f369 harness, 24-core sweep) landed in RFC 005's World 3:
queue variants indistinguishable in rq_runtime; per-wake latency is the
ceiling. Record the queue-topology closure (rq-mutex frozen as default,
benchmark-driven reopening only), define v0.9 as the wake-path latency
cycle (RFC 005 slot -> bench -> RFC 004 spinning workers -> bench +
interaction pass), and add per-switch cost to Later pending a profiling
spike + RFC.
Completed cycles compacted to the two trailing minors (v0.7, v0.8);
earlier history lives in git.
Gotchas now lead with the consuming-wake invariant ('the only wildcard wake
is request_stop') and the obligations it puts on future wakers. Deviations
recorded: the no-park exit's retire_wait/clear_notify (a protocol piece the
plan missed), Drop-guard deregistration superseded by own-pid assert
relaxation, and closed arms being ready forever.
Marks the three v0.6 items done with their hashes and the load-bearing
deviations: two-class lock order replacing the strict leaf rule, the
registry as a bimap with name_of and without send_by_name (no per-pid
mailbox to send to), and call_timeout built on recv_timeout instead of
monitor machinery. Adds the Leaf -> Channel order to the standing
invariants; README module table picks up recv_timeout, call_timeout,
and the registry row.