From 4bb7f40b7f357331cd4b50ee449a5b16a880ec5a Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 20 Mar 2024 17:06:24 +0000 Subject: [PATCH] feat(node): Allow Anr worker to be stopped and restarted --- .../suites/anr/stop-and-start.js | 50 ++++++++++++++ .../node-integration-tests/suites/anr/test.ts | 4 ++ .../src/integrations/anr/index.ts | 69 +++++++++++++++---- 3 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/anr/stop-and-start.js diff --git a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js new file mode 100644 index 000000000000..9de453abf23d --- /dev/null +++ b/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); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index 210f32588588..7ace974d6170 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -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); + }); }); diff --git a/packages/node-experimental/src/integrations/anr/index.ts b/packages/node-experimental/src/integrations/anr/index.ts index 6ef1b5d58ac3..cd18556ef23d 100644 --- a/packages/node-experimental/src/integrations/anr/index.ts +++ b/packages/node-experimental/src/integrations/anr/index.ts @@ -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'; @@ -32,33 +32,69 @@ async function getContexts(client: NodeClient): Promise { const INTEGRATION_NAME = 'Anr'; +type AnrInternal = { startWorker: () => void; stopWorker: () => void }; + const _anrIntegration = ((options: Partial = {}) => { + 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) => 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): Promise { - const contexts = await getContexts(client); +async function _startWorker( + client: NodeClient, + integrationOptions: Partial, +): 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; @@ -78,11 +114,11 @@ async function _startWorker(client: NodeClient, _options: Partial { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + }; }