From c41dd6146e20c34e0ef127bc5f88f56fe3d78d4e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:33:55 +0000 Subject: [PATCH 1/4] bun test --parallel: wake the coordinator's loop from the SIGINT/SIGTERM handler The abort handler only set SHOULD_ABORT. Coordinator::drive reads the flag between polls, and the poll retries on EINTR, so once every worker was mid-file the flag was noticed only when a housekeeping timer (GC repeating timer, Date header timer, both ~1s) happened to wake the loop: Ctrl+C or SIGTERM took up to a second to act on. Publish the coordinator's uws loop when the handler is installed and have the handler (POSIX signal handler and Windows console control handler) call us_wakeup_loop after setting the flag, so the poll returns immediately. ensure_waker() moves ahead of the handler install since it is what sets the loop pointer up. --- src/runtime/cli/test/parallel/Coordinator.rs | 41 ++++++++-- src/runtime/cli/test/parallel/runner.rs | 10 ++- test/cli/test/parallel.test.ts | 82 +++++++++++++++++++- 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/src/runtime/cli/test/parallel/Coordinator.rs b/src/runtime/cli/test/parallel/Coordinator.rs index c68a8df2d9c2..182808e8ca3b 100644 --- a/src/runtime/cli/test/parallel/Coordinator.rs +++ b/src/runtime/cli/test/parallel/Coordinator.rs @@ -7,7 +7,7 @@ use core::ffi::c_void; #[cfg(unix)] use core::mem::MaybeUninit; -use core::sync::atomic::{AtomicBool, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use std::io::Write as _; use bun_core::strings; @@ -922,15 +922,36 @@ fn describe_status<'b>(buf: &'b mut [u8; 32], status: &SpawnStatus) -> &'b [u8] } /// Coordinator-side SIGINT/SIGTERM handling. The signal handler only sets a -/// flag; `Coordinator::drive` checks it and tears down workers itself so we -/// don't do non-signal-safe work in the handler. Linux PDEATHSIG and the -/// Windows Job Object are the safety net for when the coordinator can't run -/// this (SIGKILL). +/// flag and wakes the event loop; `Coordinator::drive` checks the flag and +/// tears down workers itself so we don't do non-signal-safe work in the +/// handler. Linux PDEATHSIG and the Windows Job Object are the safety net for +/// when the coordinator can't run this (SIGKILL). pub(crate) mod abort_handler { use super::*; pub(crate) static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); + /// The loop `Coordinator::drive` parks in. Published by `install`, cleared + /// by `uninstall`; the loop itself lives until process exit. + static LOOP: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + + /// Signal context on POSIX, the console control thread on Windows. The flag + /// alone is not enough: `drive` reads it between polls and the poll retries + /// on EINTR, so an idle coordinator would only notice it when the next + /// housekeeping timer (~1s) fired. `us_wakeup_loop` is an atomic add plus an + /// eventfd write / mach_msg / kevent / uv_async_send, so it is safe to call + /// from here and makes the poll return immediately. + fn request_abort() { + SHOULD_ABORT.store(true, Ordering::Release); + let loop_ = LOOP.load(Ordering::Acquire); + if !loop_.is_null() { + // SAFETY: `install` published the live per-process uws loop, which + // outlives the coordinator; the raw extern forms no `&mut Loop`, so + // it cannot alias the tick the main thread is inside of. + unsafe { bun_uws::us_wakeup_loop(loop_) }; + } + } + // PORTING.md §Global mutable state: written once in `install()` (single // call site), read once in `uninstall()`. RacyCell — `sigaction` is POD, // no concurrent access. @@ -943,7 +964,7 @@ pub(crate) mod abort_handler { #[cfg(unix)] extern "C" fn posix_handler(_: i32, _: *const libc::siginfo_t, _: *const c_void) { - SHOULD_ABORT.store(true, Ordering::Release); + request_abort(); } #[cfg(windows)] @@ -953,7 +974,7 @@ pub(crate) mod abort_handler { use bun_sys::windows; match ctrl { windows::CTRL_C_EVENT | windows::CTRL_BREAK_EVENT | windows::CTRL_CLOSE_EVENT => { - SHOULD_ABORT.store(true, Ordering::Release); + request_abort(); windows::TRUE } _ => windows::FALSE, @@ -971,7 +992,10 @@ pub(crate) mod abort_handler { } } - pub(crate) fn install() -> Guard { + /// `loop_` is the loop `Coordinator::drive` will tick; it must already be + /// set up (`EventLoop::ensure_waker`) so the handler has something to wake. + pub(crate) fn install(loop_: *mut bun_uws::Loop) -> Guard { + LOOP.store(loop_, Ordering::Release); #[cfg(unix)] { // SAFETY: signal handler installation; PREV_* are written before @@ -1031,5 +1055,6 @@ pub(crate) mod abort_handler { bun_sys::windows::FALSE, ); } + LOOP.store(core::ptr::null_mut(), Ordering::Release); } } diff --git a/src/runtime/cli/test/parallel/runner.rs b/src/runtime/cli/test/parallel/runner.rs index e6b14c5b2100..6766cc7ebd53 100644 --- a/src/runtime/cli/test/parallel/runner.rs +++ b/src/runtime/cli/test/parallel/runner.rs @@ -191,7 +191,13 @@ pub(crate) fn run_as_coordinator( windows_job: Coordinator::create_windows_kill_on_close_job(), }; - let _abort_guard = abort_handler::install(); + // SAFETY: event_loop pointer is valid while vm lives. + let uws_loop = unsafe { + let event_loop = (*vm_ptr).event_loop(); + (*event_loop).ensure_waker(); + (*event_loop).usockets_loop() + }; + let _abort_guard = abort_handler::install(uws_loop); // Patch the Worker→Coordinator backref now that `coord`'s address is fixed. // Access workers through `coord.workers` to avoid a second &mut on the Vec. @@ -209,8 +215,6 @@ pub(crate) fn run_as_coordinator( } } - // SAFETY: event_loop pointer is valid while vm lives. - unsafe { (*(*vm_ptr).event_loop()).ensure_waker() }; // SAFETY: see vm_ptr note above. unsafe { &*vm_ptr }.run_with_api_lock(|| coord.drive()); coord.end_group(); diff --git a/test/cli/test/parallel.test.ts b/test/cli/test/parallel.test.ts index 39d0cd082d2f..26761619a06c 100644 --- a/test/cli/test/parallel.test.ts +++ b/test/cli/test/parallel.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, isASAN, isDebug, isWindows, normalizeBunSnapshot, tempDir, tls } from "harness"; +import { readFileSync } from "fs"; +import { bunEnv, bunExe, isASAN, isDebug, isLinux, isWindows, normalizeBunSnapshot, tempDir, tls } from "harness"; test("--parallel: each worker has a unique JEST_WORKER_ID and BUN_TEST_WORKER_ID", async () => { // Sleep so worker 0 is busy when workers 1/2 come online and pick up the @@ -1215,6 +1216,85 @@ test("--parallel: SIGTERM on coordinator kills workers and their grandchildren", expect(outstanding).toEqual([]); }, 15000); +// With every worker mid-file, the only thing that wakes the coordinator's poll is +// its own housekeeping (a burst of timers about once a second). A signal handler +// that does not wake the poll is therefore acted on at the next burst, 0 to ~1s +// later, depending on when the signal lands. On Linux, wait for a burst (the main +// thread's voluntary context switch count moves) and for it to settle, so the +// signal is sent at the start of the ~1s quiet period and such a coordinator +// reliably takes ~900ms; one that wakes its poll reacts at once either way. +async function waitForCoordinatorToPark(pid: number) { + const switches = () => + Number(/voluntary_ctxt_switches:\s+(\d+)/.exec(readFileSync(`/proc/${pid}/status`, "utf8"))![1]); + const deadline = Date.now() + 3000; + const before = switches(); + while (switches() === before && Date.now() < deadline) await Bun.sleep(1); + let last = switches(); + let quietSince = Date.now(); + while (Date.now() - quietSince < 100 && Date.now() < deadline) { + await Bun.sleep(1); + const now = switches(); + if (now !== last) { + last = now; + quietSince = Date.now(); + } + } +} + +test.skipIf(isWindows).concurrent.each(["SIGTERM", "SIGINT"] as const)( + "--parallel: coordinator acts on %s immediately while every worker is mid-file", + async signal => { + const fixture = ` + import { test } from "bun:test"; + import { appendFileSync } from "fs"; + test("slow", async () => { + appendFileSync(process.env.PIDS, process.pid + "\\n"); + await Bun.sleep(60000); + }); + `; + using dir = tempDir("parallel-abort-latency", { "a.test.ts": fixture, "b.test.ts": fixture }); + const pids = String(dir) + "/pids.txt"; + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "--parallel=2", "--parallel-delay=0"], + env: { ...bunEnv, PIDS: pids }, + cwd: String(dir), + stdout: "ignore", + stderr: "pipe", + }); + + const workersRunning = () => { + try { + return readFileSync(pids, "utf8").split("\n").filter(Boolean).length; + } catch { + return 0; + } + }; + while (workersRunning() < 2 && proc.exitCode === null) await Bun.sleep(10); + expect(workersRunning()).toBe(2); + if (isLinux) await waitForCoordinatorToPark(proc.pid); + + // The "Interrupted" report is the first thing the coordinator does once it + // has seen the signal; measure up to that rather than up to exit so process + // teardown (slow under ASAN) stays out of the number. + const stderr = proc.stderr.getReader(); + const decoder = new TextDecoder(); + let seen = ""; + const start = performance.now(); + proc.kill(signal); + while (!seen.includes("Interrupted")) { + const { value, done } = await stderr.read(); + if (done) break; + seen += decoder.decode(value, { stream: true }); + } + const reactedAfterMs = performance.now() - start; + + expect(seen).toContain("Interrupted"); + expect(reactedAfterMs).toBeLessThan(500); + expect(await proc.exited).toBe(130); + }, +); + test("--parallel --no-isolate: a worker keeps one global and module registry across its files", async () => { const files: Record = { "shared.ts": `export let count = 0; export const bump = () => ++count;`, From 4a6fc0fc9ded3953e96a64a9e8c3c89952a77f82 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:54:37 +0000 Subject: [PATCH 2/4] test: give the coordinator signal latency test the same timeout as its sibling --- test/cli/test/parallel.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/cli/test/parallel.test.ts b/test/cli/test/parallel.test.ts index 26761619a06c..214c01e2fe83 100644 --- a/test/cli/test/parallel.test.ts +++ b/test/cli/test/parallel.test.ts @@ -1293,6 +1293,7 @@ test.skipIf(isWindows).concurrent.each(["SIGTERM", "SIGINT"] as const)( expect(reactedAfterMs).toBeLessThan(500); expect(await proc.exited).toBe(130); }, + 15000, ); test("--parallel --no-isolate: a worker keeps one global and module registry across its files", async () => { From 8c72839891dbda21e2c37ea2c4a1ce7f8da2b3b8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:58:23 +0000 Subject: [PATCH 3/4] abort_handler: move the wakeup rationale into the module doc --- src/runtime/cli/test/parallel/Coordinator.rs | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/runtime/cli/test/parallel/Coordinator.rs b/src/runtime/cli/test/parallel/Coordinator.rs index 182808e8ca3b..58920380c61e 100644 --- a/src/runtime/cli/test/parallel/Coordinator.rs +++ b/src/runtime/cli/test/parallel/Coordinator.rs @@ -921,33 +921,28 @@ fn describe_status<'b>(buf: &'b mut [u8; 32], status: &SpawnStatus) -> &'b [u8] } } -/// Coordinator-side SIGINT/SIGTERM handling. The signal handler only sets a -/// flag and wakes the event loop; `Coordinator::drive` checks the flag and -/// tears down workers itself so we don't do non-signal-safe work in the -/// handler. Linux PDEATHSIG and the Windows Job Object are the safety net for -/// when the coordinator can't run this (SIGKILL). +/// Coordinator-side SIGINT/SIGTERM handling. The handler sets a flag and wakes +/// the event loop; `Coordinator::drive` checks the flag between polls and tears +/// down workers itself, so nothing non-signal-safe runs in the handler. The +/// wakeup is what makes the flag visible promptly: the poll retries on EINTR, +/// so without it an idle coordinator sees the flag only at its next +/// housekeeping timer, up to ~1s later. Linux PDEATHSIG and the Windows Job +/// Object are the safety net for when the coordinator can't run this (SIGKILL). pub(crate) mod abort_handler { use super::*; pub(crate) static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); - /// The loop `Coordinator::drive` parks in. Published by `install`, cleared - /// by `uninstall`; the loop itself lives until process exit. + /// Set by `install`, cleared by `uninstall`. The uws loop is process-lifetime. static LOOP: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); - /// Signal context on POSIX, the console control thread on Windows. The flag - /// alone is not enough: `drive` reads it between polls and the poll retries - /// on EINTR, so an idle coordinator would only notice it when the next - /// housekeeping timer (~1s) fired. `us_wakeup_loop` is an atomic add plus an - /// eventfd write / mach_msg / kevent / uv_async_send, so it is safe to call - /// from here and makes the poll return immediately. + /// Signal context on POSIX; the console control thread on Windows. fn request_abort() { SHOULD_ABORT.store(true, Ordering::Release); let loop_ = LOOP.load(Ordering::Acquire); if !loop_.is_null() { - // SAFETY: `install` published the live per-process uws loop, which - // outlives the coordinator; the raw extern forms no `&mut Loop`, so - // it cannot alias the tick the main thread is inside of. + // SAFETY: the loop is process-lifetime, and the raw extern is the + // thread-safe entry point (no `&mut Loop` formed off the main thread). unsafe { bun_uws::us_wakeup_loop(loop_) }; } } @@ -992,8 +987,7 @@ pub(crate) mod abort_handler { } } - /// `loop_` is the loop `Coordinator::drive` will tick; it must already be - /// set up (`EventLoop::ensure_waker`) so the handler has something to wake. + /// `loop_` is the loop `Coordinator::drive` ticks (`ensure_waker` already run). pub(crate) fn install(loop_: *mut bun_uws::Loop) -> Guard { LOOP.store(loop_, Ordering::Release); #[cfg(unix)] From 7988c6b6b8182928266bae95f58b1ac67f6aa3b9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:01:10 +0000 Subject: [PATCH 4/4] abort_handler: shorten the module doc --- src/runtime/cli/test/parallel/Coordinator.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/runtime/cli/test/parallel/Coordinator.rs b/src/runtime/cli/test/parallel/Coordinator.rs index 58920380c61e..dd65d72adf8b 100644 --- a/src/runtime/cli/test/parallel/Coordinator.rs +++ b/src/runtime/cli/test/parallel/Coordinator.rs @@ -922,12 +922,9 @@ fn describe_status<'b>(buf: &'b mut [u8; 32], status: &SpawnStatus) -> &'b [u8] } /// Coordinator-side SIGINT/SIGTERM handling. The handler sets a flag and wakes -/// the event loop; `Coordinator::drive` checks the flag between polls and tears -/// down workers itself, so nothing non-signal-safe runs in the handler. The -/// wakeup is what makes the flag visible promptly: the poll retries on EINTR, -/// so without it an idle coordinator sees the flag only at its next -/// housekeeping timer, up to ~1s later. Linux PDEATHSIG and the Windows Job -/// Object are the safety net for when the coordinator can't run this (SIGKILL). +/// the loop (the poll retries on EINTR, so the flag alone would wait for the +/// next ~1s housekeeping timer); `Coordinator::drive` does the teardown. Linux +/// PDEATHSIG and the Windows Job Object cover the case where it can't (SIGKILL). pub(crate) mod abort_handler { use super::*;