From a497454e66dd1c8a264d43e761c0b6ce08bcc636 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:51:59 +0000 Subject: [PATCH 1/2] test(worker-terminate-lifetime): exit the fetch-teardown fixture on its condition, not on a clock The worker in "worker exit with streaming-request-body fetches whose response.body was touched" exited 150 to 210 ms after it started and reported exit code 3 when no fetch had answered by then. A burst of 48 loopback connections takes 100 ms or more to establish on the macOS x64 CI hosts, so the deadline passed before the state the test tears down existed, and the test went red there in every parallel batch. The worker now exits once all eight responses have been touched. The per-worker offset is kept, anchored on that point. A fetch rejection and a generous bound (half the test timeout) still exit 3, with the reason on stderr, so a real failure is reported instead of a hang. --- .../workers/worker-terminate-lifetime.test.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/test/js/web/workers/worker-terminate-lifetime.test.ts b/test/js/web/workers/worker-terminate-lifetime.test.ts index 1f3cd6f81934..f3eeef67461d 100644 --- a/test/js/web/workers/worker-terminate-lifetime.test.ts +++ b/test/js/web/workers/worker-terminate-lifetime.test.ts @@ -966,6 +966,10 @@ test( // response stream's producer by writing through the stream's wrapper into a source that sweep had // already freed (heap-use-after-free WRITE under ASAN). The tasklet now holds a counted ref on the // source for as long as it is its producer. +// +// The worker exits once every response has been touched, not on a clock: a burst of 48 loopback +// connections takes 100ms+ to establish on the macOS x64 CI hosts, so a fixed deadline exited +// before the state it is meant to tear down existed. test( "worker exit with streaming-request-body fetches whose response.body was touched", async () => { @@ -977,23 +981,33 @@ test( const { Worker } = require("node:worker_threads"); const server = Bun.serve({ port: 0, fetch(req) { return new Response(req.body); } }); const N = 6; + const J = 8; let done = 0; for (let i = 0; i < N; i++) { const w = new Worker(\` const keep = []; let touched = 0; - for (let j = 0; j < 8; j++) { + for (let j = 0; j < \${J}; j++) { let ctrl; const body = new ReadableStream({ start(c) { ctrl = c; c.enqueue(new Uint8Array(1024)); } }); const p = fetch("\${server.url}", { method: "POST", body, duplex: "half" }) - .then((r) => { keep.push(r.body); const rd = r.body.getReader(); rd.read(); keep.push(rd); touched++; }) - .catch(() => {}); + .then((r) => { + keep.push(r.body); const rd = r.body.getReader(); rd.read(); keep.push(rd); + // Exit with that state alive: every response touched while its request body is + // still streaming. The per-worker offset only varies where the exit lands + // relative to the 5ms enqueue cadence. + if (++touched === \${J}) setTimeout(() => process.exit(0), \${(i * 13) % 60}); + }) + .catch((e) => { console.error("fetch failed:", e); process.exit(3); }); keep.push(p, ctrl); setInterval(() => { try { ctrl.enqueue(new Uint8Array(512)); } catch {} }, 5); } - // Exit with that state alive; exit code 3 if no fetch ever reached it (the test would - // then not be exercising what it claims to). - setTimeout(() => process.exit(touched > 0 ? 0 : 3), 150 + \${(i * 13) % 60}); + // Only a failure reaches this: the test would otherwise hang to its timeout without + // saying which worker never got its responses. + setTimeout(() => { + console.error("worker \${i}: " + touched + " of \${J} responses touched"); + process.exit(3); + }, ${timeout / 2}); \`, { eval: true }); w.on("error", (e) => { console.error(e); process.exit(1); }); w.on("exit", (code) => { From e5dce163b1416fd5fd752bf93f77e34cdc325aed Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:35:43 +0000 Subject: [PATCH 2/2] test(worker-terminate-lifetime): drop the per-worker exit offset The teardown state is complete at the eighth touched response. The offset could not move a stream across any state boundary, and the exit already lands at a random point in the 5 ms enqueue cadence. --- test/js/web/workers/worker-terminate-lifetime.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/js/web/workers/worker-terminate-lifetime.test.ts b/test/js/web/workers/worker-terminate-lifetime.test.ts index f3eeef67461d..d6898de8c493 100644 --- a/test/js/web/workers/worker-terminate-lifetime.test.ts +++ b/test/js/web/workers/worker-terminate-lifetime.test.ts @@ -994,9 +994,8 @@ test( .then((r) => { keep.push(r.body); const rd = r.body.getReader(); rd.read(); keep.push(rd); // Exit with that state alive: every response touched while its request body is - // still streaming. The per-worker offset only varies where the exit lands - // relative to the 5ms enqueue cadence. - if (++touched === \${J}) setTimeout(() => process.exit(0), \${(i * 13) % 60}); + // still streaming. + if (++touched === \${J}) setTimeout(() => process.exit(0), 0); }) .catch((e) => { console.error("fetch failed:", e); process.exit(3); }); keep.push(p, ctrl);