Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node): Allow Anr worker to be stopped and restarted (v7) #11228

Merged
merged 1 commit into from Mar 21, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 50 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/stop-and-start.js
@@ -0,0 +1,50 @@
const crypto = require('crypto');
const assert = require('assert');

const Sentry = require('@sentry/node');

setTimeout(() => {
process.exit();
}, 10000);

const anr = Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 });

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
debug: true,
autoSessionTracking: false,
integrations: [anr],
});

function longWorkIgnored() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
assert.ok(hash);
}
}

function longWork() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
assert.ok(hash);
}
}

setTimeout(() => {
anr.stopWorker();

setTimeout(() => {
longWorkIgnored();

setTimeout(() => {
anr.startWorker();

setTimeout(() => {
longWork();
});
}, 2000);
}, 2000);
}, 2000);
4 changes: 4 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/test.ts
Expand Up @@ -106,4 +106,8 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
test('from forked process', done => {
createRunner(__dirname, 'forker.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
});

test('worker can be stopped and restarted', done => {
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
});
});
78 changes: 64 additions & 14 deletions packages/node/src/integrations/anr/index.ts
@@ -1,7 +1,16 @@
// TODO (v8): This import can be removed once we only support Node with global URL
import { URL } from 'url';
import { convertIntegrationFnToClass, defineIntegration, getCurrentScope } from '@sentry/core';
import type { Client, Contexts, Event, EventHint, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
import type {
Client,
Contexts,
Event,
EventHint,
Integration,
IntegrationClass,
IntegrationFn,
IntegrationFnResult,
} from '@sentry/types';
import { dynamicRequire, logger } from '@sentry/utils';
import type { Worker, WorkerOptions } from 'worker_threads';
import type { NodeClient } from '../../client';
Expand Down Expand Up @@ -52,23 +61,51 @@ interface InspectorApi {

const INTEGRATION_NAME = 'Anr';

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

const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
let worker: Promise<() => void> | undefined;
let client: NodeClient | undefined;

return {
name: INTEGRATION_NAME,
// TODO v8: Remove this
setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
setup(client: NodeClient) {
startWorker: () => {
if (worker) {
return;
}

if (client) {
worker = _startWorker(client, options);
}
},
stopWorker: () => {
if (worker) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
worker.then(stop => {
stop();
worker = undefined;
});
}
},
setup(initClient: NodeClient) {
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
throw new Error('ANR detection requires Node 16.17.0 or later');
}

// setImmediate is used to ensure that all other integrations have been setup
setImmediate(() => _startWorker(client, options));
client = initClient;

// setImmediate is used to ensure that all other integrations have had their setup called first.
// This allows us to call into all integrations to fetch the full context
setImmediate(() => this.startWorker());
},
};
} as IntegrationFnResult & AnrInternal;
}) satisfies IntegrationFn;

export const anrIntegration = defineIntegration(_anrIntegration);
type AnrReturn = (options?: Partial<AnrIntegrationOptions>) => IntegrationFnResult & AnrInternal;

export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn;

/**
* Starts a thread to detect App Not Responding (ANR) events
Expand All @@ -90,14 +127,20 @@ export type Anr = typeof Anr;
/**
* Starts the ANR worker thread
*/
async function _startWorker(client: NodeClient, _options: Partial<AnrIntegrationOptions>): Promise<void> {
const contexts = await getContexts(client);
async function _startWorker(
client: NodeClient,
integrationOptions: Partial<AnrIntegrationOptions>,
): Promise<() => void> {
const dsn = client.getDsn();

if (!dsn) {
return;
return () => {
//
};
}

const contexts = await getContexts(client);

// These will not be accurate if sent later from the worker thread
delete contexts.app?.app_memory;
delete contexts.device?.free_memory;
Expand All @@ -116,11 +159,11 @@ async function _startWorker(client: NodeClient, _options: Partial<AnrIntegration
release: initOptions.release,
dist: initOptions.dist,
sdkMetadata,
appRootPath: _options.appRootPath,
pollInterval: _options.pollInterval || DEFAULT_INTERVAL,
anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD,
captureStackTrace: !!_options.captureStackTrace,
staticTags: _options.staticTags || {},
appRootPath: integrationOptions.appRootPath,
pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL,
anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD,
captureStackTrace: !!integrationOptions.captureStackTrace,
staticTags: integrationOptions.staticTags || {},
contexts,
};

Expand All @@ -139,6 +182,7 @@ async function _startWorker(client: NodeClient, _options: Partial<AnrIntegration
});

process.on('exit', () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
worker.terminate();
});

Expand Down Expand Up @@ -176,4 +220,10 @@ async function _startWorker(client: NodeClient, _options: Partial<AnrIntegration

// Ensure this thread can't block app exit
worker.unref();

return () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
worker.terminate();
clearInterval(timer);
};
}