Bun.sleepSync: make worker.terminate() interrupt a worker blocked in sleepSync
Problem
worker.terminate() never completes while the worker thread is parked in Bun.sleepSync(N). The close event does not fire until the full N milliseconds elapse, so a supervisor that relies on terminate() to bound hostile code cannot reclaim a worker that calls Bun.sleepSync with a large argument.
const w = new Worker(
"data:text/javascript," +
encodeURIComponent('postMessage("sleeping"); Bun.sleepSync(600000);'),
);
w.addEventListener("close", () => { console.log("CLOSED"); process.exit(0); });
w.addEventListener("message", () => w.terminate());
setTimeout(() => { console.log("HUNG"); process.exit(1); }, 5000);
// bun 1.4.0: prints HUNG, 4/4
Cause
sleep_sync is a single std::thread::sleep. The parent-thread WebWorker::notify_need_termination only sets requested_terminate, fires the JSC NeedTermination VMTrap (serviced at JS safepoints), and wakes the uws/uv event-loop poll. None of those can unblock a thread parked in nanosleep/Sleep, so the worker stays asleep for the full duration.
Fix
When sleepSync runs on a worker thread, slice the sleep against an Instant deadline and poll WebWorker::has_requested_terminate() between slices. Once terminate() sets the flag, sleepSync returns at the next slice boundary and the already-fired VMTrap throws the TerminationException at the next JS safepoint. Main-thread sleepSync keeps the single uninterruptible sleep (there is nothing to poll).
The slice is 100 ms: termination latency is bounded by that, and the extra wakeups are negligible (a 1 h sleep wakes ~36000 times, microseconds each).
Verification
New test in test/js/web/workers/worker-terminate-lifetime.test.ts:
| build | result |
|---|---|
USE_SYSTEM_BUN=1 bun test -t "Bun.sleepSync" | fail (HUNG) |
bun bd test -t "Bun.sleepSync" | pass (~0.8 s), 4/4 |
test/js/bun/util/sleepSync.test.ts (5 tests) passes unchanged, and sleepSync(250) in a worker still measures 250 ms (deadline loop preserves accuracy).
Related
This came out of a supervisor kill-switch audit that also flagged Atomics.wait (#32802) and nested-worker orphans. Those are independent changes.
no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/workers/worker-terminate-lifetime.test.ts