roadmap: gen_server time patterns shipped (RFC 015); substrate status recorded

This commit is contained in:
smarm-agent
2026-06-18 19:31:06 +00:00
parent f454a9195a
commit fc014c4e54
+40 -31
View File
@@ -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<A>` /
`Name<M>` *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<A>` via `send_to`, `Name<M>` 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<A>` via `send_to`, `Name<M>` 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