timer: send_after / cancel_timer message-delivery substrate

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.
This commit is contained in:
smarm-agent
2026-06-18 14:28:54 +00:00
parent 5cb7f6a491
commit 61520bf2cc
6 changed files with 356 additions and 21 deletions
+15 -8
View File
@@ -95,10 +95,17 @@ and the full 013/014 history in git.
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:
**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.
**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.
**The gen_server patterns themselves** (the headline). The OTP time vocabulary,
expressed against the v0.8 server loop and its `select` arm priority:
@@ -111,10 +118,10 @@ expressed against the v0.8 server loop and its `select` arm priority:
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.
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.
---