Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const Sentry = require('@sentry/node');
const { waitForDebuggerReady } = require('@sentry-internal/test-utils');

setTimeout(() => {
process.exit();
Expand Down Expand Up @@ -53,9 +52,11 @@ setTimeout(() => {
setTimeout(() => {
anr.startWorker();

// Wait for the restarted worker's debugger session to reconnect before blocking the event
// loop, otherwise on slow CI the worker isn't ready to sample and the ANR is missed entirely.
waitForDebuggerReady(() => {
// Wait for the restarted worker to reconnect its debugger session before blocking the event
// loop. The main-thread inspector stays open across restarts, so there is no main-thread signal
// that the new worker is ready; without this, on slow CI `longWork` can run before the worker
// is sampling and the ANR is missed entirely.
anr.waitUntilWorkerReady().then(() => {
longWork();
});
}, 2000);
Expand Down
19 changes: 16 additions & 3 deletions packages/node/src/integrations/anr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,17 @@ async function getContexts(client: NodeClient): Promise<Contexts> {

const INTEGRATION_NAME = 'Anr' as const;

type AnrInternal = { startWorker: () => void; stopWorker: () => void };
type AnrInternal = {
startWorker: () => void;
stopWorker: () => void;
waitUntilWorkerReady: () => Promise<void>;
};

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

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

if (client) {
worker = _startWorker(client, options);
const initializedClient = client;
if (initializedClient) {
workerReady = new Promise<void>(resolve => {
worker = _startWorker(initializedClient, options, resolve);
});
}
},
stopWorker: () => {
if (worker) {
workerReady = undefined;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
worker.then(stop => {
Comment on lines 92 to 98

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A race condition in disableAnrDetectionForCallback with a synchronous callback can permanently disable ANR detection because startWorker exits early before the old worker is fully terminated.
Severity: HIGH

Suggested Fix

To fix this, ensure worker is cleared before or at the same time as workerReady in stopWorker. For example, set workerReady to undefined inside the .then() block after the worker has been stopped and its promise cleared. Alternatively, modify startWorker to handle this state correctly and re-initialize the worker.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/node/src/integrations/anr/index.ts#L92-L98

Potential issue: A race condition occurs when `disableAnrDetectionForCallback` is used
with a synchronous callback. The `stopWorker` function sets `workerReady` to `undefined`
synchronously but only schedules the termination of the `worker` promise asynchronously
via a microtask. If the synchronous callback finishes quickly, `startWorker` is called
before the microtask runs. Inside `startWorker`, the check `if (worker)` is true,
causing it to return early without starting a new worker. When the microtask eventually
runs, it clears the `worker`, leaving the integration in a state with no worker running
and no `workerReady` promise. This permanently disables ANR detection for the
application's lifecycle.

Also affects:

  • packages/node/src/integrations/anr/index.ts:68~70

Did we get this right? 👍 / 👎 to inform future reviews.

stop();
worker = undefined;
});
}
},
waitUntilWorkerReady: () => workerReady ?? Promise.resolve(),
async setup(initClient: NodeClient) {
client = initClient;

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

Expand Down Expand Up @@ -234,6 +245,8 @@ async function _startWorker(
if (msg === 'session-ended') {
log('ANR event sent from ANR worker. Clearing session in this thread.');
getIsolationScope().setSession(undefined);
} else if (msg === 'worker-ready') {
onReady?.();
}
});

Expand Down
6 changes: 6 additions & 0 deletions packages/node/src/integrations/anr/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,9 @@ parentPort?.on('message', (msg: { session: Session | undefined; debugImages?: Re

poll();
});

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