4 untracked agent-briefing docs from the fed-sx-m1 worktree (merged branch loops/fed-sx-m2), saved here so they survive the worktree cleanup.
6.9 KiB
; -- mode: markdown --
loops/erlang — erlang:send_after substrate primitive
Scoped briefing for a single focused iteration loop on loops/erlang.
Not a replacement for the general erlang-loop.md; this is the
load-bearing-blocker task that, when done, unblocks
plans/fed-sx-milestone-2.md Step 8b-timer (delivery retry loop)
and the standard gen_server timeout return.
description: loops/erlang — send_after primitive
subagent_type: general-purpose
run_in_background: true
isolation: worktree # worktree at /root/rose-ash-loops/erlang
The goal
Implement the standard Erlang timer primitives so a process can schedule a message-to-self (or to another pid) after N milliseconds:
Ref = erlang:send_after(Time, Dest, Msg). %% Time: int millis. Dest: pid or atom.
ok | TimeLeft = erlang:cancel_timer(Ref). %% returns remaining ms, or false if fired/expired.
These are the same primitives gen_server uses internally to emit
{noreply, S, Timeout} returns — when the gen_server's handle_*
callback returns {noreply, NewState, T}, the gen_server schedules
{timeout} to itself after T ms via send_after, and the next
handle_info({timeout}, S) fires when no other message arrives first.
Without send_after, anything that wants a delayed self-cast has to
busy-loop or block — neither acceptable for the kernel.
Acceptance — single SX file lib/erlang/tests/send_after.sx
The standard test suite plus this one. Run the conformance gate after
each commit (bash lib/erlang/conformance.sh).
T1—erlang:send_after(50, self(), hello)returns a Ref; after 60ms a receive picks uphello. Round-trip latency under 100ms in steady state.T2—Ref = erlang:send_after(1000, self(), late), erlang:cancel_timer(Ref)returns an integer ~1000 (remaining ms) AND a subsequentreceive late -> got after 50 -> none endreturnsnone— the cancelled message never arrives.T3— multiple in-flight timers fire in deadline order, not schedule order:send_after(80, self(), b), thensend_after(20, self(), a)— selective receive onafirst.T4— cancel_timer on an already-fired timer returnsfalse.T5—send_afterto a registered atom: register a process, queue a delayed message to its name, it lands in that process's mailbox.T6—gen_server{noreply, S, 100}→handle_info({timeout}, S)fires after 100ms when no other message arrives. (Sanity check that the gen_server library, currently shipped, hooks up correctly.)
Conformance gate stays green (currently 725/725 on loops/erlang per
lib/erlang/scoreboard.json, 761/761 after the m2 BIFs are in —
both numbers will move when this lands).
Implementation shape (suggested, not prescribed)
The OCaml host has no event loop today — bin/sx_server.ml is a
single-thread epoch protocol. But the Erlang scheduler in
lib/erlang/runtime.sx IS a SX-level event loop already: er-sched
holds the global dict; er-sched-step-alive! advances one runnable
process; er-sched-run-all! drains the runnable queue until all
processes are waiting / exiting / dead. Two options:
Option A — pure-SX timer wheel. Extend the scheduler dict with
:timers (sorted-list-of {:deadline-ms _ :pid _ :msg _ :ref _ :alive true}). er-sched-run-all! already loops; when it finds the runnable
queue empty but processes still alive in receive blocks, walk the
timer list and check whether any deadline has passed (need a
monotonic clock — see below). For deadlines in the future, sleep
until the next one or until the runnable queue gets work. Cancel = set
:alive false on the timer entry; cull on next scan.
Option B — OCaml-side Unix.setitimer / Unix.select wakeup.
Heavier, lets the SX scheduler park properly on Unix.select against
a self-pipe. Right shape for a long-running kernel that may sit idle
for minutes between events. Probably the production answer, but
significantly more work than A.
Recommend A first for the unblock; B as a follow-up if the kernel needs to idle gracefully.
Clock primitive
erlang:monotonic_time/0,1 and erlang:system_time/0,1 ought to
exist for completeness — send_after needs at least a monotonic ms
clock. If the substrate doesn't have one, add erlang:monotonic_time(millisecond)
as a small native BIF in bin/sx_server.ml (Unix.gettimeofday) and
expose it via er-bif-erlang-monotonic-time. Time travel safety:
DO NOT use wall-clock for timer deadlines.
Files in play (loops/erlang scope)
lib/erlang/runtime.sx— scheduler state, BIFs, primitive registration. Scheduler dict ater-scheduler(single-element list). Step function:er-sched-step-alive!(line ~708 on m2; check line numbers freshly — recent commits moved code).lib/erlang/tests/send_after.sx— new file, the test suite above.lib/erlang/scoreboard.json+scoreboard.md— bump counts.- (If Option B is taken)
bin/sx_server.ml— out ofloops/erlangscope per the loop's standing rules. Surface a separateloops/fed-primstask for the native bits and keepsend_after's SX wrapper minimal.
Base branch / rebase note
The substrate change for the kernel mutex (Blockers #4 in
fed-sx-milestone-2.md) lands on architecture via the m2 merge. The
key change is in lib/erlang/runtime.sx:
er-sched-step-alive!reads:pending-argsfrom the process record when first invoked (line 736 on m2; was hardcoded to(list)).er-bif-http-listenspawns the user's handler as a real er-process with:pending-args (list req-pl)instead ofer-apply-fun-inline.
If loops/erlang rebases onto origin/architecture and that merge
hasn't landed yet (still sitting on local /root/rose-ash at
089ed88f-or-later), fetch origin/loops/fed-sx-m2 and cherry-pick
or rebase against 29e4234b to pick up the :pending-args field
shape. The work area (er-sched-step-alive!) is the same surface
this loop will touch — without the m2 change, the timer wakeup path
won't dovetail cleanly with the spawn-from-BIF pattern. Conflict
risk: medium. Conflict surface: small (~5 lines around the
:pending-args read).
Test discipline
bash lib/erlang/conformance.shgreen before and after every commit.- Commits scoped to
lib/erlang/**. No edits tonext/**,bin/sx_server.ml,spec/**, or otherlib/<lang>/**from this loop. - One commit per acceptance test cluster —
T1+T2,T3+T4,T5+T6is a reasonable cadence. - Push to
origin/loops/erlangafter each commit. The fed-sx-m2 loop is dormant waiting on this.
Done when
- 6/6 new
send_aftertests green. - conformance gate green at the new total.
- single line ticked in
plans/erlang-on-sx.mdProgress log + a tick on whatever roadmap entry covers timers. - Then update the m2 plan (
plans/fed-sx-milestone-2.mdBlockers #3) to RESOLVED and unblock Step 8b-timer; that update belongs to the fed-sx-m2 loop, not this one.