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 #11214

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,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);
});
});
69 changes: 55 additions & 14 deletions packages/node-experimental/src/integrations/anr/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineIntegration, getCurrentScope } from '@sentry/core';
import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/types';
import type { Contexts, Event, EventHint, IntegrationFn, IntegrationFnResult } from '@sentry/types';
import { logger } from '@sentry/utils';
import * as inspector from 'inspector';
import { Worker } from 'worker_threads';
Expand Down Expand Up @@ -32,33 +32,69 @@ async function getContexts(client: NodeClient): Promise<Contexts> {

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,
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 the ANR worker thread
*
* @returns A function to stop the worker
*/
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 @@ -78,11 +114,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 Down Expand Up @@ -135,4 +171,9 @@ 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();
};
}