;; erlang:send_after / cancel_timer — timer primitives. ;; ;; A process schedules a message to itself (or another pid / registered ;; name) after N logical milliseconds. `cancel_timer` removes a pending ;; timer and reports the time left. These are the same primitives the ;; gen_server library uses to implement `{noreply, State, Timeout}`. ;; ;; The scheduler runs a synchronous logical clock (see runtime.sx ;; `er-sched-advance-time!`): time advances only when the runnable ;; queue drains, jumping to the earliest pending deadline. That makes ;; delivery deterministic and time-travel-safe — no wall clock. (define er-sa-test-count 0) (define er-sa-test-pass 0) (define er-sa-test-fails (list)) (define er-sa-test (fn (name actual expected) (set! er-sa-test-count (+ er-sa-test-count 1)) (if (= actual expected) (set! er-sa-test-pass (+ er-sa-test-pass 1)) (append! er-sa-test-fails {:actual actual :expected expected :name name})))) (define er-sa-pred (fn (name actual) (er-sa-test name (if actual true false) true))) (define sa-ev erlang-eval-ast) ;; ── T1 — schedule a self-message, receive it after the deadline ── ;; send_after returns a reference handle. (er-sa-pred "T1 send_after returns a ref" (er-ref? (sa-ev "erlang:send_after(50, self(), hello)"))) ;; The scheduled message lands and a plain receive picks it up. (er-sa-test "T1 delivered message received" (get (sa-ev "erlang:send_after(50, self(), hello), receive M -> M end") :name) "hello") ;; Logical time advances exactly to the timer deadline (50ms) by the ;; time the message is received — round-trip latency well under 100ms. (er-sa-test "T1 clock at deadline on receipt" (sa-ev "erlang:send_after(50, self(), hello), receive hello -> erlang:monotonic_time() end") 50) ;; ── T2 — cancel_timer returns remaining ms; message never arrives ── ;; Cancel immediately after scheduling: clock has not advanced, so the ;; full duration (~1000ms) is reported as remaining. (er-sa-test "T2 cancel returns remaining ms" (sa-ev "Ref = erlang:send_after(1000, self(), late), erlang:cancel_timer(Ref)") 1000) ;; The cancelled timer never delivers — the receive falls through to ;; its `after` clause and returns `none`. (er-sa-test "T2 cancelled message never arrives" (get (sa-ev "Ref = erlang:send_after(1000, self(), late), erlang:cancel_timer(Ref), receive late -> got after 50 -> none end") :name) "none") ;; ── T3 — multiple timers fire in deadline order, not schedule order ── ;; `b` is scheduled first (deadline 80) but `a` second (deadline 20). ;; Two plain receives drain the mailbox in arrival order — and arrival ;; is governed by deadline, so the first message out is `a`. (er-sa-test "T3 timers fire in deadline order" (er-format-value (sa-ev "erlang:send_after(80, self(), b), erlang:send_after(20, self(), a), X = receive M1 -> M1 end, Y = receive M2 -> M2 end, {X, Y}")) "{a,b}") ;; A selective receive on `a` matches the earlier-deadline timer even ;; though `b` was scheduled first. (er-sa-test "T3 selective receive picks earliest deadline" (get (sa-ev "erlang:send_after(80, self(), b), erlang:send_after(20, self(), a), receive a -> first end") :name) "first") ;; ── T4 — cancel_timer on an already-fired timer returns false ────── ;; Once `x` has been received the timer has fired; cancelling its ref ;; now yields the atom `false`. (er-sa-test "T4 cancel of fired timer is false" (get (sa-ev "Ref = erlang:send_after(20, self(), x), receive x -> ok end, erlang:cancel_timer(Ref)") :name) "false") ;; ── T5 — send_after to a registered atom name ────────────────────── ;; A second process registers itself as `srv`; the timer addresses it ;; by name, and the delayed message lands in that process's mailbox. ;; The server forwards what it got back to the parent for inspection. (er-sa-test "T5 timer delivers to registered name" (get (sa-ev "Me = self(), Pid = spawn(fun () -> receive M -> Me ! {got, M} end end), register(srv, Pid), erlang:send_after(20, srv, ping), receive {got, X} -> X end") :name) "ping") ;; ── T6 — gen_server {noreply, State, Timeout} hookup ─────────────── ;; A gen_server that, on the `arm` cast, returns {noreply, S, 100}. ;; The library schedules {timeout} to itself via send_after; when no ;; other message arrives first, handle_info({timeout}, S) fires. The ;; handler signals the parent so we can confirm the timeout landed. (do (er-load-gen-server!) (erlang-load-module "-module(sa_tmo). init(Me) -> {ok, Me}. handle_call(_R, _F, S) -> {reply, ok, S}. handle_cast(arm, Me) -> {noreply, Me, 100}. handle_info({timeout}, Me) -> Me ! fired, {noreply, Me}; handle_info(_M, S) -> {noreply, S}.") nil) (er-sa-test "T6 gen_server timeout fires handle_info" (get (sa-ev "Me = self(), P = gen_server:start_link(sa_tmo, Me), gen_server:cast(P, arm), receive fired -> ok after 5000 -> timeout end") :name) "ok")