Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 45s
T3 — concurrent timers fire in deadline order, not schedule order (scheduler jumps the clock to the earliest pending deadline each time the runnable queue drains). T4 — cancel_timer on an already-fired timer returns the atom false. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
3.7 KiB
Plaintext
121 lines
3.7 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")
|