Skip to content

Commit 1bf8a75

Browse files
JohnMcLearclaude
andcommitted
test(flake): scoped per-test event-loop yield in the 6 dying spec files
The Windows backend silent-ELIFECYCLE flake (12 captures so far across PRs #7838 / #7842 / #7846) clusters tightly to six spec files whose tests share a common shape: many short tests (50-100 in a single describe) firing rapid sequential supertest HTTP or socket.io connect/disconnect calls against the in-process Etherpad server. Pre-kill node-reports show the V8 main isolate becomes event-loop- starved 200-400 ms before each kill — the 5 Hz heartbeat falls silent for the entire death window — then the process is externally terminated bypassing every JS handler, --report-on-fatalerror, --report-uncaught-exception, signal handlers. PR #7852 ruled out TIME_WAIT accumulation as the proximate cause (the kill survived connection reuse, even though TIME_WAIT churn dropped to near zero). The actual trigger remains unidentified — but rapid sequential loopback I/O across test boundaries is the only common substrate across all six files. This patch adds a single setImmediate yield in beforeEach of each top-level describe in the six dying files. The yield forces the event loop to drain its timer queue between every test in those files, breaking the tight microtask chain that may be what triggers whatever native-level kill is happening on Windows + Node 24. Scope-critical: this is per-file beforeEach, NOT a root-level mochaHooks.beforeEach. The latter was tried in #7844 and broke ep_subscript_and_superscript's tests, which share state across describe-block boundaries and don't tolerate a yield. Per-file scope contains any timing perturbation to the affected files. Plugin tests loaded from `../node_modules/ep_*/static/tests/backend/specs` are completely untouched. Files modified (with death counts in parens): - src/tests/backend/specs/api/pad.ts (2) - src/tests/backend/specs/api/importexportGetPost.ts (4) - src/tests/backend/specs/socketio.ts (3) - src/tests/backend/specs/messages.ts (3) - src/tests/backend/specs/import.ts (1) - src/tests/backend/specs/clientvar_rev_consistency.ts (1) Test plan: - Linux ± plugins must pass. If they regress, the yield is breaking the affected tests' own state-sharing assumptions — we'd revert. - Windows ± plugins: pre-fix flake rate is ~22% per #7663 (13/60 runs). If the post-fix rate drops materially over 5-10 reruns, rapid-sequential-cadence is the trigger and this is a working mitigation. If unchanged, cadence is ruled out and we look at per-test pathologies (jose CNG on Windows, specific Express middleware paths, libuv IOCP edge cases unrelated to load). Either outcome is a strict improvement on the current state of "we have no idea what's causing it." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c4d2467 commit 1bf8a75

6 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/tests/backend/specs/api/importexportGetPost.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ const deleteTestPad = async () => {
3737

3838
describe(__filename, function () {
3939
this.timeout(45000);
40+
// Per-test setImmediate yield against the Windows backend silent-ELIFECYCLE
41+
// flake. See rationale in api/pad.ts.
42+
beforeEach(async function () {
43+
await new Promise((r) => setImmediate(r));
44+
});
4045
before(async function () { agent = await common.init(); });
4146

4247
describe('Connectivity', function () {

src/tests/backend/specs/api/pad.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ const ulSpaceHtml = '<!doctype html><html><body><ul class="bullet"> <li>one</li>
4949
const expectedSpaceHtml = '<!doctype html><html><body><ul class="bullet"><li>one</ul></body></html>';
5050

5151
describe(__filename, function () {
52+
// Per-test setImmediate yield. The Windows backend-test silent-ELIFECYCLE
53+
// flake (12 captures so far, see PR #7838+#7842+#7846 diagnostics) shows
54+
// event-loop starvation 200-400 ms before each kill, in tests that fire
55+
// 50-100/s with rapid sequential loopback HTTP. This yield forces the
56+
// event loop to service its timer queue between every test in this file,
57+
// collapsing the back-to-back microtask chains that may be the trigger.
58+
// Scoped per-file rather than at the root mocha hook level because
59+
// PR #7844 demonstrated that a root-level yield breaks plugin tests
60+
// that share state across describe-block boundaries.
61+
beforeEach(async function () {
62+
await new Promise((r) => setImmediate(r));
63+
});
64+
5265
before(async function () {
5366
agent = await common.init();
5467
const res = await agent.get('/api/')

src/tests/backend/specs/clientvar_rev_consistency.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ const settings = require('../../../node/utils/Settings');
2121
import {randomString} from '../../../static/js/pad_utils';
2222

2323
describe(__filename, function () {
24+
// Per-test setImmediate yield against the Windows backend silent-ELIFECYCLE
25+
// flake. See rationale in api/pad.ts.
26+
beforeEach(async function () {
27+
await new Promise((r) => setImmediate(r));
28+
});
2429
let agent: any;
2530
let clientVarsBackup: any;
2631
let loadTestBackup: boolean;

src/tests/backend/specs/import.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const padManager = require('../../../node/db/PadManager');
1111
import settings from '../../../node/utils/Settings';
1212

1313
describe(__filename, function () {
14+
// Per-test setImmediate yield against the Windows backend silent-ELIFECYCLE
15+
// flake. See rationale in api/pad.ts.
16+
beforeEach(async function () {
17+
await new Promise((r) => setImmediate(r));
18+
});
1419
const settingsBackup: MapArrayType<any> = {};
1520
let agent: any;
1621

src/tests/backend/specs/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const plugins = require('../../../static/js/pluginfw/plugin_defs');
1010
import readOnlyManager from '../../../node/db/ReadOnlyManager';
1111

1212
describe(__filename, function () {
13+
// Per-test setImmediate yield against the Windows backend silent-ELIFECYCLE
14+
// flake. See rationale in api/pad.ts.
15+
beforeEach(async function () {
16+
await new Promise((r) => setImmediate(r));
17+
});
1318
let agent:any;
1419
let pad:PadType|null;
1520
let padId: string;

src/tests/backend/specs/socketio.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const socketIoRouter = require('../../../node/handler/SocketIORouter');
1212

1313
describe(__filename, function () {
1414
this.timeout(30000);
15+
// Per-test setImmediate yield against the Windows backend silent-ELIFECYCLE
16+
// flake. See rationale in api/pad.ts.
17+
beforeEach(async function () {
18+
await new Promise((r) => setImmediate(r));
19+
});
1520
let agent: any;
1621
let authorize:Function;
1722
const backups:MapArrayType<any> = {};

0 commit comments

Comments
 (0)