Skip to content

Commit add5d5f

Browse files
authored
feat(node): Allow Anr worker to be stopped and restarted (v7) (#11228)
Backport of #11214
1 parent 4caa946 commit add5d5f

File tree

3 files changed

+118
-14
lines changed

3 files changed

+118
-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
@@ -106,4 +106,8 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
106106
test('from forked process', done => {
107107
createRunner(__dirname, 'forker.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
108108
});
109+
110+
test('worker can be stopped and restarted', done => {
111+
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
112+
});
109113
});

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

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
// TODO (v8): This import can be removed once we only support Node with global URL
22
import { URL } from 'url';
33
import { convertIntegrationFnToClass, defineIntegration, getCurrentScope } from '@sentry/core';
4-
import type { Client, Contexts, Event, EventHint, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
4+
import type {
5+
Client,
6+
Contexts,
7+
Event,
8+
EventHint,
9+
Integration,
10+
IntegrationClass,
11+
IntegrationFn,
12+
IntegrationFnResult,
13+
} from '@sentry/types';
514
import { dynamicRequire, logger } from '@sentry/utils';
615
import type { Worker, WorkerOptions } from 'worker_threads';
716
import type { NodeClient } from '../../client';
@@ -52,23 +61,51 @@ interface InspectorApi {
5261

5362
const INTEGRATION_NAME = 'Anr';
5463

64+
type AnrInternal = { startWorker: () => void; stopWorker: () => void };
65+
5566
const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
67+
let worker: Promise<() => void> | undefined;
68+
let client: NodeClient | undefined;
69+
5670
return {
5771
name: INTEGRATION_NAME,
5872
// TODO v8: Remove this
5973
setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
60-
setup(client: NodeClient) {
74+
startWorker: () => {
75+
if (worker) {
76+
return;
77+
}
78+
79+
if (client) {
80+
worker = _startWorker(client, options);
81+
}
82+
},
83+
stopWorker: () => {
84+
if (worker) {
85+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
86+
worker.then(stop => {
87+
stop();
88+
worker = undefined;
89+
});
90+
}
91+
},
92+
setup(initClient: NodeClient) {
6193
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
6294
throw new Error('ANR detection requires Node 16.17.0 or later');
6395
}
6496

65-
// setImmediate is used to ensure that all other integrations have been setup
66-
setImmediate(() => _startWorker(client, options));
97+
client = initClient;
98+
99+
// setImmediate is used to ensure that all other integrations have had their setup called first.
100+
// This allows us to call into all integrations to fetch the full context
101+
setImmediate(() => this.startWorker());
67102
},
68-
};
103+
} as IntegrationFnResult & AnrInternal;
69104
}) satisfies IntegrationFn;
70105

71-
export const anrIntegration = defineIntegration(_anrIntegration);
106+
type AnrReturn = (options?: Partial<AnrIntegrationOptions>) => IntegrationFnResult & AnrInternal;
107+
108+
export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn;
72109

73110
/**
74111
* Starts a thread to detect App Not Responding (ANR) events
@@ -90,14 +127,20 @@ export type Anr = typeof Anr;
90127
/**
91128
* Starts the ANR worker thread
92129
*/
93-
async function _startWorker(client: NodeClient, _options: Partial<AnrIntegrationOptions>): Promise<void> {
94-
const contexts = await getContexts(client);
130+
async function _startWorker(
131+
client: NodeClient,
132+
integrationOptions: Partial<AnrIntegrationOptions>,
133+
): Promise<() => void> {
95134
const dsn = client.getDsn();
96135

97136
if (!dsn) {
98-
return;
137+
return () => {
138+
//
139+
};
99140
}
100141

142+
const contexts = await getContexts(client);
143+
101144
// These will not be accurate if sent later from the worker thread
102145
delete contexts.app?.app_memory;
103146
delete contexts.device?.free_memory;
@@ -116,11 +159,11 @@ async function _startWorker(client: NodeClient, _options: Partial<AnrIntegration
116159
release: initOptions.release,
117160
dist: initOptions.dist,
118161
sdkMetadata,
119-
appRootPath: _options.appRootPath,
120-
pollInterval: _options.pollInterval || DEFAULT_INTERVAL,
121-
anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD,
122-
captureStackTrace: !!_options.captureStackTrace,
123-
staticTags: _options.staticTags || {},
162+
appRootPath: integrationOptions.appRootPath,
163+
pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL,
164+
anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD,
165+
captureStackTrace: !!integrationOptions.captureStackTrace,
166+
staticTags: integrationOptions.staticTags || {},
124167
contexts,
125168
};
126169

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

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

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

177221
// Ensure this thread can't block app exit
178222
worker.unref();
223+
224+
return () => {
225+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
226+
worker.terminate();
227+
clearInterval(timer);
228+
};
179229
}

0 commit comments

Comments
 (0)