Skip to content

Commit 05ee48e

Browse files
mydeaclaude
andcommitted
test(node): Fix flaky ANR restart test with deterministic worker-ready signal
The `stop-and-start` ANR test restarts the worker and then blocks the event loop, expecting the restarted worker to sample and report the ANR. It previously ran `longWork` almost immediately after `startWorker()`. `waitForDebuggerReady` (polling `inspector.url()`) cannot gate this: the ANR integration opens the main-thread inspector once and never closes it, so `inspector.url()` stays truthy across restarts and the poll returns immediately. `_startWorker` is also async, so the new worker may not even be spawned yet. On slow CI the event loop can block before the new worker reconnects its inspector session, and the ANR is missed. The worker now posts a `worker-ready` message once it is fully set up, and the integration exposes `waitUntilWorkerReady()` so the restart path can await the new worker's actual readiness instead of a fixed delay. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9851d64 commit 05ee48e

3 files changed

Lines changed: 27 additions & 6 deletions

File tree

‎dev-packages/node-integration-tests/suites/anr/stop-and-start.js‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ setTimeout(() => {
5353
setTimeout(() => {
5454
anr.startWorker();
5555

56-
// Wait for the restarted worker's debugger session to reconnect before blocking the event
57-
// loop, otherwise on slow CI the worker isn't ready to sample and the ANR is missed entirely.
58-
waitForDebuggerReady(() => {
56+
// Wait for the restarted worker to reconnect its debugger session before blocking the event
57+
// loop. The main-thread inspector stays open across restarts, so there is no main-thread signal
58+
// that the new worker is ready; without this, on slow CI `longWork` can run before the worker
59+
// is sampling and the ANR is missed entirely.
60+
anr.waitUntilWorkerReady().then(() => {
5961
longWork();
6062
});
6163
}, 2000);

‎packages/node/src/integrations/anr/index.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,17 @@ async function getContexts(client: NodeClient): Promise<Contexts> {
6060

6161
const INTEGRATION_NAME = 'Anr' as const;
6262

63-
type AnrInternal = { startWorker: () => void; stopWorker: () => void };
63+
type AnrInternal = {
64+
startWorker: () => void;
65+
stopWorker: () => void;
66+
waitUntilWorkerReady: () => Promise<void>;
67+
};
6468

6569
// eslint-disable-next-line typescript/no-deprecated
6670
const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
6771
let worker: Promise<() => void> | undefined;
6872
let client: NodeClient | undefined;
73+
let workerReady: Promise<void> | undefined;
6974

7075
// Hookup the scope fetch function to the global object so that it can be called from the worker thread via the
7176
// debugger when it pauses
@@ -79,19 +84,24 @@ const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
7984
return;
8085
}
8186

82-
if (client) {
83-
worker = _startWorker(client, options);
87+
const initializedClient = client;
88+
if (initializedClient) {
89+
workerReady = new Promise<void>(resolve => {
90+
worker = _startWorker(initializedClient, options, resolve);
91+
});
8492
}
8593
},
8694
stopWorker: () => {
8795
if (worker) {
96+
workerReady = undefined;
8897
// eslint-disable-next-line @typescript-eslint/no-floating-promises
8998
worker.then(stop => {
9099
stop();
91100
worker = undefined;
92101
});
93102
}
94103
},
104+
waitUntilWorkerReady: () => workerReady ?? Promise.resolve(),
95105
async setup(initClient: NodeClient) {
96106
client = initClient;
97107

@@ -157,6 +167,7 @@ async function _startWorker(
157167
client: NodeClient,
158168
// eslint-disable-next-line typescript/no-deprecated
159169
integrationOptions: Partial<AnrIntegrationOptions>,
170+
onReady?: () => void,
160171
): Promise<() => void> {
161172
const dsn = client.getDsn();
162173

@@ -234,6 +245,8 @@ async function _startWorker(
234245
if (msg === 'session-ended') {
235246
log('ANR event sent from ANR worker. Clearing session in this thread.');
236247
getIsolationScope().setSession(undefined);
248+
} else if (msg === 'worker-ready') {
249+
onReady?.();
237250
}
238251
});
239252

‎packages/node/src/integrations/anr/worker.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,9 @@ parentPort?.on('message', (msg: { session: Session | undefined; debugImages?: Re
327327

328328
poll();
329329
});
330+
331+
// Signal that the worker is fully set up: the inspector session (when capturing stack traces) is
332+
// connected to the main thread and the watchdog is armed. Consumers that restart the worker can wait
333+
// for this before blocking the event loop, since the main-thread inspector stays open across restarts
334+
// and gives no signal that the new worker has reconnected.
335+
parentPort?.postMessage('worker-ready');

0 commit comments

Comments
 (0)