Skip to content

Commit

Permalink
feat(node): Allow Anr worker to be stopped and restarted (getsentry#1…
Browse files Browse the repository at this point in the history
…1214)

With the Electron SDK, we have an issue
(getsentry/sentry-electron#830) where ANR
causes the app to freeze when the machine goes through a suspend/resume
cycle.

This PR exposes `stopWorker` and `startWorker` methods which can be used
in the downstream [ANR integration
wrapper](https://github.com/getsentry/sentry-electron/blob/master/src/main/integrations/anr.ts)
to stop the ANR feature before device suspend and start it again on
device resume.
  • Loading branch information
timfish authored and cadesalaberry committed Apr 19, 2024
1 parent d2a1e2a commit 726d21e
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 14 deletions.
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();
};
}

0 comments on commit 726d21e

Please sign in to comment.