Skip to content

wasi-shims: re-arm the timer pollable's sleep; chunk below the setTimeout ceiling - #78

Merged
lann merged 1 commit into
mainfrom
timer-rearm-chunk
Aug 11, 2026
Merged

wasi-shims: re-arm the timer pollable's sleep; chunk below the setTimeout ceiling#78
lann merged 1 commit into
mainfrom
timer-rearm-chunk

Conversation

@lann

@lann lann commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Review finding on #71: Pollable.timer cached its wake promise forever (armed ??=, never reset). ready() consults the clock, so an early-firing sleep left the cached promise permanently settled while ready() stayed false — block()/poll()'s re-check loops then await an already-settled promise in a tight cycle: a hot microtask spin that starves the event loop, i.e. the livelock class the parking kernel exists to kill.

Severe trigger: the engine setTimeout ceiling — delays > 2^31−1 ms clamp to ~0 (node/Deno warn, fire at 1 ms; verified empirically) — so any deadline past ~24.8 days (far-future timeouts, u64 sentinels) spun hot for its whole duration. Ordinary timer slop (early by a tick) caused the same spin briefly.

Fix: one in-flight sleep shared by concurrent waiters (promise-swap contract unchanged), re-armed after every settle with the delta recomputed; each sleep capped at TIMER_CHUNK_MAX_MS (2^31−1) so far deadlines sleep in ceiling-sized chunks, re-checking the clock at each chunk end.

Pins (both fail-on-pre-fix, verified via stash/unstash): frozen-clock early fire → next wait() must be a fresh pending promise; +60-day deadline → wait still parked 30 ms in (pre-fix: settled at ~1 ms). sanitizeOps: false on exactly these two — the re-armed sleeps outlive the tests by design (the WIT surface has no timer cancel).

Gates: just test-wasi-shims 52/0 (+2 new), just websocket-conformance 55/55 at baseline wall time (14.4 s).

…eout ceiling

Pollable.timer cached its wake promise forever (armed ??= with no
reset). ready() consults the clock, so any EARLY-firing sleep left the
cached promise permanently settled while ready() stayed false — and
block()/poll()'s re-check loops (await an already-settled promise,
re-check, repeat) degenerate to a hot microtask spin that starves the
event loop: the livelock class the parking kernel exists to kill.

The severe trigger is the engine's setTimeout ceiling: delays above
2^31-1 ms are clamped to ~0 (node/Deno warn and fire at 1 ms), so any
deadline past ~24.8 days — far-future timeouts, u64 sentinels — spun
hot for its whole duration. Ordinary timer slop (early by a tick)
caused the same spin briefly.

Fix: one in-flight sleep shared by concurrent waiters, re-armed after
every settle with the delta recomputed, each sleep capped at the
ceiling (TIMER_CHUNK_MAX_MS) — far deadlines sleep in chunks and
re-check the clock at each chunk end.

Pins (both fail-on-pre-fix, verified by stash/unstash): a frozen-clock
early fire must yield a fresh PENDING promise from the next wait();
a +60-day deadline's wait is still parked 30 ms in (pre-fix the clamp
settled it at ~1 ms). sanitizeOps off for exactly these two: the
re-armed sleeps outlive the tests by design (nothing in the WIT
surface cancels a timer).

Gates: test-wasi-shims 52/0 (+2), websocket-conformance 55/55 at
baseline wall time.
@lann
lann merged commit 54c6376 into main Aug 11, 2026
4 checks passed
@lann
lann deleted the timer-rearm-chunk branch August 11, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant