Skip to content

Bun.sleepSync: make worker.terminate() interrupt a worker blocked in sleepSync - #35103

Open
robobun wants to merge 1 commit into
mainfrom
farm/ec7c553a/sleepsync-worker-terminate
Open

Bun.sleepSync: make worker.terminate() interrupt a worker blocked in sleepSync#35103
robobun wants to merge 1 commit into
mainfrom
farm/ec7c553a/sleepsync-worker-terminate

Conversation

@robobun

@robobun robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

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

…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.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 16c79724-06b5-404c-a8d0-ce44f88c3387

📥 Commits

Reviewing files that changed from the base of the PR and between 47597ab and da0b4ac.

📒 Files selected for processing (2)
  • src/runtime/api/BunObject.rs
  • test/js/web/workers/worker-terminate-lifetime.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:57 AM PT - Jul 22nd, 2026

@robobun, your commit da0b4ac has 2 failures in Build #77671 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35103

That installs a local version of the PR into your bun-35103 executable, so you can run:

bun-35103 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused fix that slices sleepSync in workers so terminate() can take effect within 100 ms.

What was reviewed

  • milliseconds is a non-negative i32 before the as u64 cast, so the switch from try_from().expect() is behavior-preserving; Instant + duration cannot overflow at ≤ i32 ms.
  • worker_ref() lifetime is documented to outlive the VM and has_requested_terminate() is an atomic Acquire load, so the cross-thread poll is safe.
  • Deadline-based loop preserves total sleep accuracy; terminate-before-sleep race is handled by the top-of-loop flag check.
  • Test follows the file's existing spawn/drain/assert pattern, is hermetic, and uses the shared timeout.
Extended reasoning...

Overview

Two-file change: src/runtime/api/BunObject.rs replaces the single std::thread::sleep in sleep_sync with a deadline-based loop when running on a worker thread, polling WebWorker::has_requested_terminate() between 100 ms slices. Main-thread behavior is unchanged. A regression test is added to test/js/web/workers/worker-terminate-lifetime.test.ts that verifies worker.terminate() unblocks a worker parked in Bun.sleepSync(600000).

Security risks

None. No untrusted input handling changes — argument validation (number check, negative check) is untouched. The only new state read is an existing AtomicBool via an Acquire load, which is the intended cross-thread contract for requested_terminate. worker_ref() returns a &WebWorker whose backing allocation is documented to outlive the VM (src/jsc/VirtualMachine.rs:910-917), and this call happens on the worker's own thread, so there is no lifetime hazard.

Level of scrutiny

Medium. It touches worker-thread coordination, but the change is additive and narrow: it reads one existing atomic flag between sleep slices. I checked the as u64 cast (safe: milliseconds: i32 is validated >= 0 immediately above), Instant::now() + duration overflow (max i32 ms ≈ 24.8 days, well within platform limits), and the terminate-races-sleep-entry case (the flag is checked before each slice, so an early terminate returns immediately).

Other factors

The test mirrors the four existing tests in the same file exactly (subprocess spawn, concurrent stdout/stderr/exited drain, exact-string asserts, shared timeout). The PR description reports it fails under USE_SYSTEM_BUN=1 and passes on the debug build, and that test/js/bun/util/sleepSync.test.ts still passes. The deadline loop (vs. accumulating fixed slices) preserves total sleep duration accuracy, which the description also verified empirically.

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

CI on build #77671: 191 lanes passed, 3 failed, 2 darwin lanes still queued. The new test in worker-terminate-lifetime.test.ts passed on every lane that ran.

The three failed lanes are debian 13 x64 and two ubuntu 25.04 x64 shards, each on one of these GC-timing tests:

  • test/js/node/test/parallel/test-net-connect-memleak.js
  • test/js/node/test/parallel/test-gc-http-client-connaborted.js

Both fail intermittently on main (present in main builds 77601 and 77580, absent in 77597/77574/77571/77565/77564/77561) and neither touches workers or Bun.sleepSync. The remaining annotations are marked flaky (compile-windows-metadata.test.ts, bun-upgrade.test.ts ETXTBSY, test-http-server-connections-checking-leak.js).

This diff is ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants