From fc014c4e54fa381a8f8b47aa5a70ef8c3dea0df7 Mon Sep 17 00:00:00 2001 From: smarm-agent Date: Thu, 18 Jun 2026 19:31:06 +0000 Subject: [PATCH] roadmap: gen_server time patterns shipped (RFC 015); substrate status recorded --- ROADMAP.md | 71 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 47bf681..6d372dc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -90,38 +90,39 @@ and the full 013/014 history in git. --- -## Up next — gen_server time-related patterns +## gen_server time-related patterns ✅ SHIPPED (RFC 015) +*(layer 2; seven commits `47d75d1`→`f454a91`, each a reviewable chunk. Builds on +the `send_after` substrate below.)* -Now unblocked (RFC 013/014 shipped: a timer's `Dest` resolves to a `Pid` / -`Name` *on fire*, not a bespoke channel/closure). Two layers: +The OTP time vocabulary against the v0.8 server loop, with **no handler-signature +change** — the capability is a handle stashed on `self` (the `Watcher` pattern), +not a return-directive or a `&ctx` threaded through handlers. New trait surface: +`type Timer` (server's own scheduled payload, `()` if unused, kept distinct from +the external `type Info`), `handle_timer`, `handle_idle` (both no-op defaults). -**Substrate — `send_after` / `cancel_timer`.** ✅ SHIPPED — message-delivery -timer on the existing `timer.rs` min-heap: deliver a value to an address -(`Pid` via `send_to`, `Name` via `send`), resolved *on fire* so a dead -target / restarted name is observed at fire time; failed resolve dropped -(Erlang `erlang:send_after`). A new `Reason::Send { fire }` thunk carries -delivery type-erased; cancellation is an `armed` set keyed on the entry `seq` -(only `Send` timers use it — `Sleep`/`WaitTimeout` keep inert-stale), exposed -as an opaque `TimerId`. `cancel` is unscoped (any holder of the id) and returns -whether it landed before the fire (the race signal). `peek_deadline`'s contract -relaxed to "≤ true next deadline" so a future hierarchical timing wheel can back -`Timers` without touching `send_after` or the scheduler idle path. +- **One-shot / debounce / retry-backoff** — `ctx.timer()` hands out a clonable + `TimerHandle`; `arm_after(d, msg)` arms, `cancel(id)` carries the substrate's + race bool. Debounce/backoff are just arm-and-`cancel` against the latest event + (no special mechanism). +- **Periodic tick / heartbeat** — `tick_every(d, msg)`: loop-managed sugar over + the one-shot substrate (re-arm at `now + d`), one stable id, `cancel` stops the + re-arm *and* the pending instance. Requires `Timer: Clone` (method-level bound + only); the loop re-delivers via a stored factory, so the bound never leaks onto + `type Timer` or the loop. +- **Idle / receive timeout** — *not* a channel: it is the timeout on the loop's + `select_timeout` / `recv_timeout`. `ctx.idle_after(d)` (set once in `init`) + fixes the window; reset on any dispatched message; `None` from the wait ⇒ + `handle_idle`; re-arms steady. No generation-tag race — there is no token. -**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. The `send_after` substrate is now landed (above); `gen_statem` -state timeouts (Low priority) sit on the same mechanism. Still needs an RFC: the -timer-arming surface on `ServerCtx`, the timeout-directive shape, and how a -fired timer interacts with `handle_info` / arm priority. +Mechanics: control (monitor intake) + armed timers fold into one loop-internal +`Sys` channel selected above the inbox, so **armed timers outrank infos** (a +heartbeat can't be starved). One substrate addition — `send_after_to` (a +channel-targeting sibling of `send_after`, lands the fire on the loop's own arm). +Exit is leak-free: the drop guard (same one that runs `terminate`) drains and +cancels every live timer id, then `debug_assert!`s none survive. `call_timeout` +is unchanged — it's a *client-side* call deadline, disambiguated in docs from the +server-side idle timeout (same word, two axes). `gen_statem` state timeouts +(deferred, Low) will reuse the loop-owned idle deadline. --- @@ -151,8 +152,16 @@ 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 -Promoted to **Up next** (gen_server time-related patterns) — it's the substrate -that layer lands on, now unblocked by RFC 013/014. +✅ SHIPPED (`61520bf`) — message-delivery timer on the `timer.rs` min-heap: +deliver a value to an address (`Pid` via `send_to`, `Name` via `send`), +resolved *on fire* so a dead target / restarted name is observed at fire time; +failed resolve dropped (Erlang `erlang:send_after`). `Reason::Send { fire }` +carries delivery type-erased; cancellation is an `armed` set keyed on entry +`seq` (only `Send` uses it — `Sleep`/`WaitTimeout` stay inert-stale), exposed as +an opaque `TimerId`; `cancel` is unscoped and returns the race signal. +`peek_deadline` relaxed to "≤ true next deadline" for a future timing wheel. The +gen_server time layer (RFC 015, shipped above) lands on this, adding only the +channel-targeting `send_after_to` sibling. #### Introspection — process_info / get_state / tree dump `trace.rs` is the seed. What an actor is parked on, queue depth, stack size; a