From da0b4ac3899510b2326dc7b8730f8681c6400889 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:01:02 +0000 Subject: [PATCH] Bun.sleepSync: poll worker termination between slices so terminate() can interrupt it A worker blocked in Bun.sleepSync(N) could not be terminated until N elapsed: sleep_sync was a single std::thread::sleep, and worker.terminate()'s only wakeups are a JSC VMTrap (checked at JS safepoints) and an event-loop poke, neither of which unblocks a parked nanosleep/Sleep. In a worker, slice the sleep and check WebWorker::has_requested_terminate() between slices; terminate() now takes effect within one slice. Main-thread sleepSync is unchanged. --- src/runtime/api/BunObject.rs | 29 +++++++++++-- .../workers/worker-terminate-lifetime.test.ts | 43 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 77180e3042cb..f0169eef4ac9 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -1140,9 +1140,32 @@ pub(crate) fn sleep_sync( ))); } - std::thread::sleep(core::time::Duration::from_millis( - u64::try_from(milliseconds).expect("int cast"), - )); + let duration = core::time::Duration::from_millis(milliseconds as u64); + + // In a worker, std::thread::sleep cannot be interrupted by worker.terminate(): + // the parent thread's notify_need_termination only fires a JSC VMTrap (checked + // at JS safepoints) and wakes the event loop poll, neither of which unblocks a + // parked nanosleep/Sleep. Slice the sleep and poll the termination flag between + // slices so terminate() takes effect within SLEEP_SYNC_TERMINATE_SLICE instead + // of the full requested duration. The VMTrap then throws TerminationException + // at the next safepoint after we return. + if let Some(worker) = global_object.bun_vm().worker_ref() { + const SLEEP_SYNC_TERMINATE_SLICE: core::time::Duration = + core::time::Duration::from_millis(100); + let deadline = std::time::Instant::now() + duration; + loop { + if worker.has_requested_terminate() { + break; + } + let now = std::time::Instant::now(); + if now >= deadline { + break; + } + std::thread::sleep((deadline - now).min(SLEEP_SYNC_TERMINATE_SLICE)); + } + } else { + std::thread::sleep(duration); + } Ok(JSValue::UNDEFINED) } diff --git a/test/js/web/workers/worker-terminate-lifetime.test.ts b/test/js/web/workers/worker-terminate-lifetime.test.ts index 9d476d1f3d53..91355dd4bb02 100644 --- a/test/js/web/workers/worker-terminate-lifetime.test.ts +++ b/test/js/web/workers/worker-terminate-lifetime.test.ts @@ -176,3 +176,46 @@ test.skipIf(!isASAN)( }, timeout, ); + +// Bun.sleepSync was a single uninterruptible std::thread::sleep, so a worker +// parked in a long sleepSync never observed the parent's terminate(): VMTraps +// only fire at JS safepoints and the event-loop wakeup cannot unblock nanosleep. +test( + "terminate() interrupts a worker blocked in Bun.sleepSync", + async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const w = new Worker( + "data:text/javascript," + encodeURIComponent( + 'postMessage("sleeping"); Bun.sleepSync(600000);' + ), + ); + w.addEventListener("close", () => { + console.log("CLOSED"); + process.exit(0); + }); + // postMessage enqueue -> return -> sleepSync is synchronous on the worker + // thread; by the time this handler runs on the parent, the worker is + // already inside the sleep. + w.addEventListener("message", () => w.terminate()); + setTimeout(() => { + console.log("HUNG"); + process.exit(1); + }, 10000); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("CLOSED\n"); + expect(exitCode).toBe(0); + }, + timeout, +);