Skip to content

Commit be09f89

Browse files
authored
feat(node): Allow Anr worker to be stopped and restarted (#11214)
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.
1 parent 523b006 commit be09f89

File tree

3 files changed

+109
-14
lines changed

3 files changed

+109
-14
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const crypto = require('crypto');
2+
const assert = require('assert');
3+
4+
const Sentry = require('@sentry/node');
5+
6+
setTimeout(() => {
7+
process.exit();
8+
}, 10000);
9+
10+
const anr = Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 });
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
release: '1.0',
15+
debug: true,
16+
autoSessionTracking: false,
17+
integrations: [anr],
18+
});
19+
20+
function longWorkIgnored() {
21+
for (let i = 0; i < 20; i++) {
22+
const salt = crypto.randomBytes(128).toString('base64');
23+
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
24+
assert.ok(hash);
25+
}
26+
}
27+
28+
function longWork() {
29+
for (let i = 0; i < 20; i++) {
30+
const salt = crypto.randomBytes(128).toString('base64');
31+
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
32+
assert.ok(hash);
33+
}
34+
}
35+
36+
setTimeout(() => {
37+
anr.stopWorker();
38+
39+
setTimeout(() => {
40+
longWorkIgnored();
41+
42+
setTimeout(() => {
43+
anr.startWorker();
44+
45+
setTimeout(() => {
46+
longWork();
47+
});
48+
}, 2000);
49+
}, 2000);
50+
}, 2000);

dev-packages/node-integration-tests/suites/anr/test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,8 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
101101
test('from forked process', done => {
102102
createRunner(__dirname, 'forker.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
103103
});
104+
105+
test('worker can be stopped and restarted', done => {
106+
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
107+
});
104108
});

packages/node-experimental/src/integrations/anr/index.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineIntegration, getCurrentScope } from '@sentry/core';
2-
import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/types';
2+
import type { Contexts, Event, EventHint, IntegrationFn, IntegrationFnResult } from '@sentry/types';
33
import { logger } from '@sentry/utils';
44
import * as inspector from 'inspector';
55
import { Worker } from 'worker_threads';
@@ -32,33 +32,69 @@ async function getContexts(client: NodeClient): Promise<Contexts> {
3232

3333
const INTEGRATION_NAME = 'Anr';
3434

35+
type AnrInternal = { startWorker: () => void; stopWorker: () => void };
36+
3537
const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
38+
let worker: Promise<() => void> | undefined;
39+
let client: NodeClient | undefined;
40+
3641
return {
3742
name: INTEGRATION_NAME,
38-
setup(client: NodeClient) {
43+
startWorker: () => {
44+
if (worker) {
45+
return;
46+
}
47+
48+
if (client) {
49+
worker = _startWorker(client, options);
50+
}
51+
},
52+
stopWorker: () => {
53+
if (worker) {
54+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
55+
worker.then(stop => {
56+
stop();
57+
worker = undefined;
58+
});
59+
}
60+
},
61+
setup(initClient: NodeClient) {
3962
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
4063
throw new Error('ANR detection requires Node 16.17.0 or later');
4164
}
4265

43-
// setImmediate is used to ensure that all other integrations have been setup
44-
setImmediate(() => _startWorker(client, options));
66+
client = initClient;
67+
68+
// setImmediate is used to ensure that all other integrations have had their setup called first.
69+
// This allows us to call into all integrations to fetch the full context
70+
setImmediate(() => this.startWorker());
4571
},
46-
};
72+
} as IntegrationFnResult & AnrInternal;
4773
}) satisfies IntegrationFn;
4874

49-
export const anrIntegration = defineIntegration(_anrIntegration);
75+
type AnrReturn = (options?: Partial<AnrIntegrationOptions>) => IntegrationFnResult & AnrInternal;
76+
77+
export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn;
5078

5179
/**
5280
* Starts the ANR worker thread
81+
*
82+
* @returns A function to stop the worker
5383
*/
54-
async function _startWorker(client: NodeClient, _options: Partial<AnrIntegrationOptions>): Promise<void> {
55-
const contexts = await getContexts(client);
84+
async function _startWorker(
85+
client: NodeClient,
86+
integrationOptions: Partial<AnrIntegrationOptions>,
87+
): Promise<() => void> {
5688
const dsn = client.getDsn();
5789

5890
if (!dsn) {
59-
return;
91+
return () => {
92+
//
93+
};
6094
}
6195

96+
const contexts = await getContexts(client);
97+
6298
// These will not be accurate if sent later from the worker thread
6399
delete contexts.app?.app_memory;
64100
delete contexts.device?.free_memory;
@@ -78,11 +114,11 @@ async function _startWorker(client: NodeClient, _options: Partial<AnrIntegration
78114
release: initOptions.release,
79115
dist: initOptions.dist,
80116
sdkMetadata,
81-
appRootPath: _options.appRootPath,
82-
pollInterval: _options.pollInterval || DEFAULT_INTERVAL,
83-
anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD,
84-
captureStackTrace: !!_options.captureStackTrace,
85-
staticTags: _options.staticTags || {},
117+
appRootPath: integrationOptions.appRootPath,
118+
pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL,
119+
anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD,
120+
captureStackTrace: !!integrationOptions.captureStackTrace,
121+
staticTags: integrationOptions.staticTags || {},
86122
contexts,
87123
};
88124

@@ -135,4 +171,9 @@ async function _startWorker(client: NodeClient, _options: Partial<AnrIntegration
135171

136172
// Ensure this thread can't block app exit
137173
worker.unref();
174+
175+
return () => {
176+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
177+
worker.terminate();
178+
};
138179
}

0 commit comments

Comments
 (0)