Skip to content

Commit 571f8a4

Browse files
committed
fix(test): queue full-suite runs instead of warning about the contention
The contention warning was not enough. It scrolls past, the run starts anyway, and nothing prevents the next agent from doing the same: four concurrent suites drove load average to 10 and stretched a ~210s run past 13 minutes, which reads as a hang rather than as CPU starvation. The detection was already here and already correct -- it just did not act. `bun run test` now waits for the other runners to finish before starting. Agents working in parallel worktrees each believe they are the only runner, so the serialization has to live in the runner rather than in anyone's discipline. Queueing rather than refusing is deliberate. A hard failure invites `bun test ./tests/` directly, which bypasses this file entirely and puts the contention right back. Waiting is the behavior that survives being worked around. Two escape hatches keep it from becoming its own hang: `OCX_TEST_NO_QUEUE=1` for anyone who genuinely wants overlap, and a 45-minute ceiling after which a holder is presumed wedged and this run proceeds regardless. Verified both directions: with a competing runner present the suite waits and starts nothing (20s observed, no test output), and with the opt-out set the same invocation runs immediately (1 pass).
1 parent 95d8ed7 commit 571f8a4

1 file changed

Lines changed: 49 additions & 8 deletions

File tree

scripts/test.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,59 @@ function findCompetingTestRunners(selfPid: number): number[] {
6464
}
6565
}
6666

67+
/**
68+
* Wait until this machine has no other full-suite runner, then proceed.
69+
*
70+
* Warning about contention was not enough: the warning scrolls past, the run still
71+
* starts, and four concurrent suites drove load average to 10 and turned a ~210s
72+
* suite into a 13-minute one that read as a hang. Agents in parallel worktrees each
73+
* think they are the only runner, so the serialization has to live here rather than
74+
* in anyone's discipline.
75+
*
76+
* Queue rather than refuse: a failed `bun run test` invites `bun test` directly,
77+
* which bypasses this file entirely. Waiting is the behavior that survives being
78+
* worked around. `OCX_TEST_NO_QUEUE=1` opts out for anyone who really wants overlap.
79+
*/
80+
async function waitForExclusiveRun(selfPid: number): Promise<void> {
81+
if (process.env.OCX_TEST_NO_QUEUE === "1") return;
82+
const pollMs = 5_000;
83+
// Long enough for a full suite plus slack; past this, assume the holder is wedged
84+
// rather than working and let this run start anyway.
85+
const maxWaitMs = 45 * 60 * 1000;
86+
const startedAt = Date.now();
87+
let announced = false;
88+
for (;;) {
89+
const competing = findCompetingTestRunners(selfPid);
90+
if (competing.length === 0) {
91+
if (announced) {
92+
console.warn(`[test] the other runner(s) finished after ${Math.round((Date.now() - startedAt) / 1000)}s; starting.`);
93+
}
94+
return;
95+
}
96+
if (Date.now() - startedAt > maxWaitMs) {
97+
console.warn(
98+
`[test] still waiting on pid ${competing.join(", ")} after ${Math.round(maxWaitMs / 60000)} minutes. `
99+
+ "Assuming they are stuck and starting anyway; expect a slow run.",
100+
);
101+
return;
102+
}
103+
if (!announced) {
104+
announced = true;
105+
console.warn(
106+
`[test] ${competing.length} other bun test runner(s) already running (pid ${competing.join(", ")}). `
107+
+ "Waiting for them to finish so the suites do not fight over the CPU. "
108+
+ "Set OCX_TEST_NO_QUEUE=1 to run concurrently anyway.",
109+
);
110+
}
111+
await Bun.sleep(pollMs);
112+
}
113+
}
114+
67115
if (import.meta.main) {
68116
const isolated = createIsolatedTestEnvironment();
69117
try {
70118
const requestedTests = process.argv.slice(2);
71-
const competing = findCompetingTestRunners(process.pid);
72-
if (competing.length > 0) {
73-
console.warn(
74-
`[test] ${competing.length} other bun test runner(s) are already running (pid ${competing.join(", ")}). `
75-
+ "They share this machine's CPU, so this run will be much slower than usual and can look hung. "
76-
+ "Stop them first if that is not what you meant.",
77-
);
78-
}
119+
await waitForExclusiveRun(process.pid);
79120
const startedAt = Date.now();
80121
const child = Bun.spawnSync(
81122
[process.execPath, "test", "--isolate", ...(requestedTests.length > 0 ? requestedTests : ["./tests/"])],

0 commit comments

Comments
 (0)