Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,8 +863,10 @@ impl WebWorker {
let promise = match vm.as_mut().load_entry_point_for_web_worker(path) {
Ok(p) => p,
Err(_) => {
// process.exit() may have run during load; don't clobber its code.
if !self.exit_called.load(Ordering::Relaxed) {
// Neither a process.exit() during load nor the parent's terminate() is a load failure.
if !self.exit_called.load(Ordering::Relaxed)
&& !self.termination_requested_by_parent()
{
vm.as_mut().exit_handler.exit_code = 1;
}
self.flush_logs(vm);
Expand Down Expand Up @@ -1093,6 +1095,14 @@ impl WebWorker {
&& !self.exit_called.load(Ordering::Relaxed)
}

/// The parent's `terminate()` asked this thread to stop. Worker thread. Taken under the
/// `vm_handle` lock that `request_termination` holds while it sets both flags, so a thread
/// that already saw `requested_terminate` never reads a stale `false` here.
Comment thread
robobun marked this conversation as resolved.
Resolved
fn termination_requested_by_parent(&self) -> bool {
let _handle = self.vm_handle.lock();
self.terminated_by_parent.load(Ordering::Relaxed)
}

/// process.exit() inside the worker. Worker-thread only.
///
/// Takes `&self` (not `&mut self`) because `request_termination` /
Expand Down
22 changes: 21 additions & 1 deletion test/js/web/workers/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,24 @@ describe("web worker", () => {
w.terminate();
});

// The 'close' code is what the worker passed to process.exit(), or 0. A worker stopped by its
// parent did not choose one, whether terminate() lands while the entry module is still
// evaluating or once its loop is idle.
test.each([
// The first message is posted from the entry's top-level code, which then never yields.
["busy evaluating its entry module", "postMessage('up'); for (;;) {}"],
// A reply to a parent message comes from the worker's event loop, after the entry has loaded.
["idle in its event loop", "self.onmessage = () => postMessage('up'); setInterval(() => {}, 1000)"],
])("close code is 0 after terminate() of a worker that is %s", async (_, body) => {
const w = new Worker("data:text/javascript," + encodeURIComponent(body));
w.postMessage("ping");
await once(w, "message");
const closed = once(w, "close");
w.terminate();
const [event] = await closed;
expect({ code: event.code, wasClean: event.wasClean }).toEqual({ code: 0, wasClean: true });
});

// As in browsers and Node: not an error, the message is dropped.
test("postMessage() to a terminated worker is a no-op", async () => {
const w = new Worker("data:text/javascript,postMessage('up')");
Expand Down Expand Up @@ -679,6 +697,8 @@ describe("web worker", () => {
// after the request must release, not build script values under it.
test("terminate() while fs.readFile completions keep arriving", async () => {
using dir = tempDir("worker-readfile-churn", { "f.bin": Buffer.alloc(65536, 7) });
// Each round boots 4 workers; a debug build spends ~0.5s per round, so it runs fewer.
const rounds = isDebug ? 4 : 12;
await using proc = Bun.spawn({
cmd: [
bunExe(),
Expand All @@ -687,7 +707,7 @@ describe("web worker", () => {
let n = 0; (function pump(){ while (n < 16) { n++; readFile(\${JSON.stringify(process.argv[1])}, () => { n--; setImmediate(pump) }) } })();
postMessage("busy")\`;
const url = URL.createObjectURL(new Blob([src]));
for (let r = 0; r < 12; r++) await Promise.all(Array.from({ length: 4 }, (_, i) => new Promise(res => {
for (let r = 0; r < ${rounds}; r++) await Promise.all(Array.from({ length: 4 }, (_, i) => new Promise(res => {
const w = new Worker(url); w.addEventListener("close", res); w.onmessage = () => setTimeout(() => w.terminate(), (r + i) % 10) })));
console.log("PASS");`,
path.join(String(dir), "f.bin"),
Expand Down
Loading