Files
rose-ash/lib/erlang/tests/send_after.sx
giles b10e55f04f
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 33s
erlang: send_after to registered name + gen_server timeout returns (T5+T6, 771/771)
T5 — send_after addresses a registered atom name; the delayed message
lands in that process's mailbox (destination resolved at fire time,
dead/unregistered targets drop silently).

T6 — gen_server loop now handles the {reply,R,S,T} / {noreply,S,T}
timeout-bearing callback returns by scheduling {timeout} to itself via
send_after; handle_info({timeout}, S) fires when no other message
arrives first. Sanity-checks the library hookup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:53:08 +00:00

164 lines
5.2 KiB
Plaintext

;; 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")