Skip to content

Commit f5ac627

Browse files
authored
fix(v8/node): Enforce that ContextLines integration does not leave open file handles (#14997)
v8 backport of #14995
1 parent 286f6b0 commit f5ac627

File tree

12 files changed

+292
-6
lines changed

12 files changed

+292
-6
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13-
Work in this release was contributed by @HHK1. Thank you for your contribution!
13+
Work in this release was contributed by @HHK1 and @mstrokin. Thank you for your contribution!
1414

1515
## 8.48.0
1616

dev-packages/node-integration-tests/suites/contextLines/test.ts dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { join } from 'path';
2-
import { conditionalTest } from '../../utils';
3-
import { createRunner } from '../../utils/runner';
2+
import { conditionalTest } from '../../../utils';
3+
import { createRunner } from '../../../utils/runner';
44

55
conditionalTest({ min: 18 })('ContextLines integration in ESM', () => {
66
test('reads encoded context lines from filenames with spaces', done => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Sentry from '@sentry/node';
2+
3+
export function captureException(i: number): void {
4+
Sentry.captureException(new Error(`error in loop ${i}`));
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { captureException } from './nested-file';
2+
3+
export function runSentry(): void {
4+
for (let i = 0; i < 10; i++) {
5+
captureException(i);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { execSync } from 'node:child_process';
2+
import * as path from 'node:path';
3+
4+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
5+
import * as Sentry from '@sentry/node';
6+
7+
Sentry.init({
8+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
9+
release: '1.0',
10+
transport: loggingTransport,
11+
});
12+
13+
import { runSentry } from './other-file';
14+
15+
runSentry();
16+
17+
const lsofOutput = execSync(`lsof -p ${process.pid}`, { encoding: 'utf8' });
18+
const lsofTable = lsofOutput.split('\n');
19+
const mainPath = __dirname.replace(`${path.sep}suites${path.sep}contextLines${path.sep}memory-leak`, '');
20+
const numberOfLsofEntriesWithMainPath = lsofTable.filter(entry => entry.includes(mainPath));
21+
22+
// There should only be a single entry with the main path, otherwise we are leaking file handles from the
23+
// context lines integration.
24+
if (numberOfLsofEntriesWithMainPath.length > 1) {
25+
// eslint-disable-next-line no-console
26+
console.error('Leaked file handles detected');
27+
// eslint-disable-next-line no-console
28+
console.error(lsofTable);
29+
process.exit(1);
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { conditionalTest } from '../../../utils';
2+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
3+
4+
conditionalTest({ min: 18 })('ContextLines integration in CJS', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
// Regression test for: https://github.com/getsentry/sentry-javascript/issues/14892
10+
test('does not leak open file handles', done => {
11+
createRunner(__dirname, 'scenario.ts')
12+
.expectN(10, {
13+
event: {},
14+
})
15+
.start(done);
16+
});
17+
});

dev-packages/node-integration-tests/test.txt

+213
Large diffs are not rendered by default.

dev-packages/node-integration-tests/utils/runner.ts

+6
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ export function createRunner(...paths: string[]) {
168168
expectedEnvelopes.push(expected);
169169
return this;
170170
},
171+
expectN: function (n: number, expected: Expected) {
172+
for (let i = 0; i < n; i++) {
173+
expectedEnvelopes.push(expected);
174+
}
175+
return this;
176+
},
171177
expectHeader: function (expected: ExpectedEnvelopeHeader) {
172178
if (!expectedEnvelopeHeaders) {
173179
expectedEnvelopeHeaders = [];

packages/node/src/integrations/contextlines.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,21 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output:
142142
input: stream,
143143
});
144144

145+
// We need to explicitly destroy the stream to prevent memory leaks,
146+
// removing the listeners on the readline interface is not enough.
147+
// See: https://github.com/nodejs/node/issues/9002 and https://github.com/getsentry/sentry-javascript/issues/14892
148+
function destroyStreamAndResolve(): void {
149+
stream.destroy();
150+
resolve();
151+
}
152+
145153
// Init at zero and increment at the start of the loop because lines are 1 indexed.
146154
let lineNumber = 0;
147155
let currentRangeIndex = 0;
148156
const range = ranges[currentRangeIndex];
149157
if (range === undefined) {
150158
// We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging.
151-
resolve();
159+
destroyStreamAndResolve();
152160
return;
153161
}
154162
let rangeStart = range[0];
@@ -162,14 +170,14 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output:
162170
DEBUG_BUILD && logger.error(`Failed to read file: ${path}. Error: ${e}`);
163171
lineReaded.close();
164172
lineReaded.removeAllListeners();
165-
resolve();
173+
destroyStreamAndResolve();
166174
}
167175

168176
// We need to handle the error event to prevent the process from crashing in < Node 16
169177
// https://github.com/nodejs/node/pull/31603
170178
stream.on('error', onStreamError);
171179
lineReaded.on('error', onStreamError);
172-
lineReaded.on('close', resolve);
180+
lineReaded.on('close', destroyStreamAndResolve);
173181

174182
lineReaded.on('line', line => {
175183
lineNumber++;

0 commit comments

Comments
 (0)