Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a0b9da3b50 fix(workers): don't unref parent event loop prematurely on worker exit
When a worker called process.exit() immediately after postMessage(),
the parent event loop could exit before processing the queued messages.
This happened because notifyNeedTermination() unreffed the parent event
loop before the worker had fully terminated and dispatched its exit/close
events. The parent's deinit() already handles unreffing correctly after
all cleanup is done.

Closes #14144

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 02:55:40 +00:00
2 changed files with 74 additions and 4 deletions

View File

@@ -438,7 +438,8 @@ fn onUnhandledRejection(vm: *jsc.VirtualMachine, globalObject: *jsc.JSGlobalObje
WebWorker__dispatchError(globalObject, worker.cpp_worker, bun.String.cloneUTF8(array.written()), error_instance);
if (vm.worker) |worker_| {
_ = worker.setRequestedTerminate();
worker.parent_poll_ref.unrefConcurrently(worker.parent);
// Don't unref parent here - deinit() in exitAndDeinit() handles it
// after dispatching exit/close events to the parent.
worker_.exitAndDeinit();
}
}
@@ -574,11 +575,15 @@ pub fn notifyNeedTermination(this: *WebWorker) callconv(.c) void {
if (this.vm) |vm| {
vm.eventLoop().wakeup();
// TODO(@190n) notifyNeedTermination
}
// TODO(@190n) delete
this.setRefInternal(false);
// Do NOT unref the parent event loop here. The parent must stay alive
// until the worker has fully terminated and dispatched its exit/close
// events. The unref happens later in deinit(), which runs after
// exitAndDeinit() has dispatched all pending events to the parent.
// Unreffing here caused a race condition where the parent event loop
// could exit before processing queued messages from postMessage()
// that were sent just before process.exit() (issue #14144).
}
/// This handles cleanup, emitting the "close" event, and deinit.

View File

@@ -0,0 +1,65 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Regression test for https://github.com/oven-sh/bun/issues/14144
// Worker messages and close events were lost when a worker called
// process.exit() immediately after postMessage(), because the parent
// event loop was unreffed too early in notifyNeedTermination().
test("worker postMessage followed by process.exit delivers all messages", async () => {
// Run the test multiple times to catch the race condition reliably.
// The original bug had a ~77% failure rate on release builds.
for (let i = 0; i < 10; i++) {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const workerBody = \`
self.postMessage({type: "start"});
self.addEventListener("message", function(event) {
let message = JSON.parse(event.data);
self.postMessage({type: "finish"});
process.exit();
});
\`;
const blob = new Blob([workerBody], {type: "application/javascript"});
const url = URL.createObjectURL(blob);
const workersCount = 2;
let finished = 0;
let closed = 0;
function checkDone() {
if (finished === workersCount && closed === workersCount) {
console.log("ALL_DONE");
}
}
for (let i = 0; i < workersCount; i++) {
const w = new Worker(url);
w.addEventListener("message", (event) => {
if (event.data.type === "finish") {
finished++;
checkDone();
}
});
w.addEventListener("close", () => {
closed++;
checkDone();
});
w.postMessage(JSON.stringify({}));
}
`,
],
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).toInclude("ALL_DONE");
expect(exitCode).toBe(0);
}
}, 30_000);