From 5cb7f6a491c0e4b2b4867e76a141500e53805485 Mon Sep 17 00:00:00 2001 From: smarm-agent Date: Thu, 18 Jun 2026 12:32:12 +0000 Subject: [PATCH] roadmap: 013/014 shipped, gen_server time patterns up next 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. --- ROADMAP.md | 97 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6252ff0..c36505a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -50,24 +50,71 @@ Consequences: --- -## Up next — RFC 013: typed addressable mailboxes (send to pid / name) +## Typed addressable mailboxes ✅ SHIPPED (RFC 013 + RFC 014) -Design agreed; full spec persisted in SMARM (`rfc_013-typed-addressable-mailboxes.md`) -and at `/root/`. The unblocker several later items quietly assume: today a `Pid` -isn't messageable, so `whereis → Pid` is near-useless and there is no send-to-pid/ -name primitive. RFC 013 reworks `registry.rs` from a name↔pid *bimap* into a -name→**live mailbox** directory off the cold leaf, with two typed addressing modes -— `Pid` (direct, identity-bound) and `Name` (durable, re-resolving, -location-transparent). Compile-time typing is preserved via phantom tokens over -*contained* `Box` erasure; the global-enum alternative was rejected -because it breaks library-extensibility (users outside the crate define actors). -Sequenced **before `send_after`** (whose `Dest` becomes a `Pid` / `Name` -resolved on fire), is the foundation for clustering's "addressed by name not pid" -migratable gen_servers, and makes the registry observer-ready for introspection -(entry carries pid + `&'static str` msg-type). Note this extends — does not retire -— the standing "select exists; a unified per-process mailbox still does not" -invariant: 013 adds addressable *delivery*, not a unified inbox; multi-port stays -`select` composition over several named channels. +The unblocker several later items quietly assumed: a `Pid` is now messageable. +RFC 013 reworked `registry.rs` from a name↔pid *bimap* into a name→**live +mailbox** directory off the cold leaf; RFC 014 layered the typed addressing and +producers on top. Two addressing modes — `Pid` (direct, identity-bound) and +`Name` (durable, re-resolving, location-transparent) — compile-time typing +preserved via phantom tokens over *contained* `Box` erasure (the +global-enum alternative was rejected: it breaks library-extensibility for +out-of-crate actors). Channel store keyed by message `TypeId` in every path. + +Delivered surface: +- **Sends:** `send_to` (`Pid`), `send` (`Name`), `send_dyn` (bare-pid + escape hatch, names the message type) — earlier 014 phases. +- **Producers & discovery** (final phase, `a866e34`): `spawn_addr` (typed-path + producer; parent-side inbox publish so an immediate `send_to` resolves, no + race on the body); `lookup_as` / `pick_as` / `members_as` (unchecked-but-sound + re-type of an erased pid — a wrong `A` degrades to `NoChannel`, never + misdelivery); `dispatch` (pick-a-live-member-and-send, `SendError::NoMember` + on empty pool). +- **By-name gen_servers** (final phase): `ServerName` over the existing + typed-channel store (keyed by `TypeId::of::>()`, so `Envelope` + stays private and no separate directory is needed); type-state + `NamedServerBuilder` (fallible `start`, `NameTaken`) leaving the infallible + `ServerBuilder::start` untouched; free `call` / `cast` / `whereis_server`; + `ServerRef::shutdown` + free `shutdown` as the sys-style synchronous stop. +- **Root-exit teardown** (final phase): the run's initial actor is the root; + when it exits, the scheduler's idle verdict stops the parked-forever remainder + (deferred past the queue drain, so actors with in-flight work finish rather + than unwinding on the stop). Closes the "app actor blocks AllDone" stall — see + Look into, below. + +Extends — does not retire — the "select exists; a unified per-process mailbox +still does not" invariant: 014 adds addressable *delivery*, not a unified inbox; +multi-port stays `select` composition over named channels. Examples: +`examples/{typed_actor,named_genserver,worker_pool}.rs`. Earlier-phase commits +and the full 013/014 history in git. + +--- + +## Up next — gen_server time-related patterns + +Now unblocked (RFC 013/014 shipped: a timer's `Dest` resolves to a `Pid` / +`Name` *on fire*, not a bespoke channel/closure). Two layers: + +**Substrate — `send_after` / `cancel_timer`.** Message-delivery timer on the +existing min-heap (`timer.rs`): deliver a value to an address at a deadline, +cancellable. Small, and the thing the gen_server idioms below have no clean +expression without today. + +**The gen_server patterns themselves** (the headline). The OTP time vocabulary, +expressed against the v0.8 server loop and its `select` arm priority: +- **Idle / receive timeout** — a callback directive that arms a one-shot so the + server gets a `timeout` if its inbox stays quiet (session expiry, idle + shutdown). Folds into the existing arm-priority ordering. +- **Periodic tick / heartbeat** — a `ServerCtx` timer arm that re-arms, for + liveness probes and metrics flushes. +- **Debounce / retry backoff** — arm-and-cancel against the latest event; + exponential re-arm on failure. + +Call deadlines already exist in part (`CallTimeoutError`); fold them in rather +than reinvent. Needs an RFC: the timer-arming surface on `ServerCtx`, the +timeout-directive shape, and how a fired timer interacts with `handle_info` / +arm priority. Land the `send_after` substrate first; `gen_statem` state timeouts +(Low priority) then sit on the same mechanism. --- @@ -97,11 +144,8 @@ profiling spike (where do the cycles actually go per park/unpark round-trip) and then an RFC before it can be scheduled. #### send_after / cancel_timer -Message-delivery timer on the existing min-heap (`timer.rs`): deliver a value to a -channel at a deadline, cancellable. Unlocks the gen_server idioms with no clean -expression today — heartbeat, debounce, retry backoff, session expiry. Small. -**Depends on RFC 013** (sequenced ahead of it): `Dest` resolves to a `Pid` / -`Name` *on fire*, not a bespoke channel/closure — see 013's send_after note. +Promoted to **Up next** (gen_server time-related patterns) — it's the substrate +that layer lands on, now unblocked by RFC 013/014. #### Introspection — process_info / get_state / tree dump `trace.rs` is the seed. What an actor is parked on, queue depth, stack size; a @@ -182,9 +226,16 @@ RFCs, not one. Spine settled in discussion; decisions still open: ## Look into -### app actors block AllDone; no external stop path +### app actors block AllDone; no external stop path — ADDRESSED (RFC 014 root-exit teardown) Agent working on urus (see same git server as smarm) reported a lazily spawned actor never returning, blocking program shutdown. Maybe we should do something about it. Agent worked around it by giving the actor an atomic bool to spin on. See urus example crud for exact impl. +**Update (RFC 014):** root-exit teardown stops the parked-forever remainder when +the root actor exits, so a lazily spawned daemon no longer wedges shutdown on +`live_actors > 0`; `ServerRef::shutdown` (+ free `shutdown`) is the explicit stop +path the atomic-bool workaround stood in for. Re-check the urus crud repro to +confirm the workaround can be retired (the teardown is cooperative — an actor in +a tight loop with no observation point still can't be stopped). + --- ## Invariants & gotchas (respect these across all cycles)