From ea0216493e70b497aae3c9ecf8640cb171a8d940 Mon Sep 17 00:00:00 2001 From: Nick Oliver Date: Wed, 26 Feb 2020 19:49:10 -0700 Subject: [PATCH] feat(server): use native V8 heapdump replaces binary module dependency --- .gitignore | 1 + __tests__/integration/helpers/testRunner.js | 58 ++++- __tests__/integration/one-app.spec.js | 51 ++++- __tests__/server/config/env/runTime.spec.js | 9 - .../utils/__snapshots__/heapdump.spec.js.snap | 3 + __tests__/server/utils/heapdump.spec.js | 203 +++++++++++------- package-lock.json | 174 +++++++++++++-- package.json | 3 - prod-sample/docker-compose.yml | 1 + src/server/config/env/runTime.js | 5 - src/server/utils/heapdump.js | 46 ++-- 11 files changed, 416 insertions(+), 138 deletions(-) create mode 100644 __tests__/server/utils/__snapshots__/heapdump.spec.js.snap diff --git a/.gitignore b/.gitignore index 100ed5a0..c4495a68 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ prod-sample/nginx/origin-statics *.key sample-module-bundles docker-compose.test.yml +*.heapsnapshot # misc .DS_Store diff --git a/__tests__/integration/helpers/testRunner.js b/__tests__/integration/helpers/testRunner.js index 714be560..eb0edca8 100644 --- a/__tests__/integration/helpers/testRunner.js +++ b/__tests__/integration/helpers/testRunner.js @@ -28,7 +28,11 @@ const deepMergeObjects = require('../../../src/server/utils/deepMergeObjects'); const prodSampleDir = path.resolve('./prod-sample/'); const pathToDockerComposeTestFile = path.resolve(prodSampleDir, 'docker-compose.test.yml'); -const setUpTestRunner = async ({ oneAppLocalPortToUse, oneAppMetricsLocalPortToUse } = {}) => { +const setUpTestRunner = async ({ + oneAppLocalPortToUse, + oneAppMetricsLocalPortToUse, + skipBrowser = false, +} = {}) => { const pathToBaseDockerComposeFile = path.resolve(prodSampleDir, 'docker-compose.yml'); const seleniumServerPort = getRandomPortNumber(); // create docker compose file from base with changes needed for tests @@ -53,8 +57,11 @@ const setUpTestRunner = async ({ oneAppLocalPortToUse, oneAppMetricsLocalPortToU }, } ); - - delete testDockerComposeFileContents.services['selenium-chrome'].entrypoint; + if (skipBrowser) { + delete testDockerComposeFileContents.services['selenium-chrome']; + } else { + delete testDockerComposeFileContents.services['selenium-chrome'].entrypoint; + } fs.writeFileSync(pathToDockerComposeTestFile, yaml.safeDump(testDockerComposeFileContents)); @@ -73,7 +80,7 @@ const setUpTestRunner = async ({ oneAppLocalPortToUse, oneAppMetricsLocalPortToU try { await Promise.all([ oneAppLocalPortToUse ? waitUntilServerIsUp(`https://localhost:${oneAppLocalPortToUse}/success`, serverStartupTimeout) : Promise.resolve(), - waitUntilServerIsUp(`http://localhost:${seleniumServerPort}`, serverStartupTimeout), + skipBrowser ? Promise.resolve() : waitUntilServerIsUp(`http://localhost:${seleniumServerPort}`, serverStartupTimeout), ]); } catch (err) { // logWatcherDuplex will buffer the logs until piped out. @@ -84,6 +91,10 @@ const setUpTestRunner = async ({ oneAppLocalPortToUse, oneAppMetricsLocalPortToU ); } + if (skipBrowser) { + return {}; + } + const browser = await remote({ logLevel: 'silent', protocol: 'http', @@ -102,7 +113,7 @@ const setUpTestRunner = async ({ oneAppLocalPortToUse, oneAppMetricsLocalPortToU return { browser }; }; -const tearDownTestRunner = async ({ browser }) => { +const tearDownTestRunner = async ({ browser } = {}) => { fs.removeSync(pathToDockerComposeTestFile); if (browser) { await browser.deleteSession(); } @@ -116,7 +127,44 @@ const tearDownTestRunner = async ({ browser }) => { } }; +function spawnAsync(...args) { + const [, , options = {}] = args; + return new Promise((res, rej) => { + const spawnedProcess = childProcess.spawn(...args); + + spawnedProcess + .on('error', rej) + .on('close', (exitCode) => { + if (exitCode !== 0) { + return rej(exitCode); + } + return res(exitCode); + }); + + const { stdio = ['pipe', 'pipe', 'pipe'] } = options; + if (stdio[1] === 'pipe') { + spawnedProcess.stderr.pipe(process.stderr); + } + if (stdio[2] === 'pipe') { + spawnedProcess.stdout.pipe(process.stdout); + } + }); +} + +function sendSignal(service, signal = 'SIGKILL') { + return spawnAsync( + 'docker-compose', + [ + '-f', pathToDockerComposeTestFile, + 'kill', + '-s', signal, + service, + ] + ); +} + module.exports = { setUpTestRunner, tearDownTestRunner, + sendSignal, }; diff --git a/__tests__/integration/one-app.spec.js b/__tests__/integration/one-app.spec.js index 4f3fccab..60beb9e0 100644 --- a/__tests__/integration/one-app.spec.js +++ b/__tests__/integration/one-app.spec.js @@ -16,11 +16,14 @@ // Headers are under a key with a dangling underscore /* eslint-disable no-underscore-dangle */ +import { promises as fs } from 'fs'; +import path from 'path'; + import fetch from 'cross-fetch'; import yargs, { argv } from 'yargs'; import parsePrometheusTextFormat from 'parse-prometheus-text-format'; -import { setUpTestRunner, tearDownTestRunner } from './helpers/testRunner'; +import { setUpTestRunner, tearDownTestRunner, sendSignal } from './helpers/testRunner'; import { waitFor } from './helpers/wait'; import { deployBrokenModule, @@ -1329,3 +1332,49 @@ describe('Scan app instance for console errors', () => { }); } }); + +describe('heapdump', () => { + const oneAppLocalPortToUse = getRandomPortNumber(); + + beforeAll(async () => { + await setUpTestRunner({ oneAppLocalPortToUse, skipBrowser: true }); + }); + + afterAll(async () => { + await tearDownTestRunner(); + await waitFor(500); + }); + + it('writes a heapdump to /tmp on a SIGUSR2 signal', async () => { + const tmpMountDir = path.resolve(__dirname, '../../prod-sample/one-app/tmp'); + // set up log watchers first to avoid semblance of a race condition + const aboutToWritePromise = searchForNextLogMatch(/about to write a heapdump to .+/); + // slower in Travis than on local machines + const didWritePromise = searchForNextLogMatch(/wrote heapdump out to .+/, 20e3); + await sendSignal('one-app', 'SIGUSR2'); + + const aboutToWriteRaw = await aboutToWritePromise; + const didWriteRaw = await didWritePromise; + + const aboutToWriteFilePath = aboutToWriteRaw + .replace(/^about to write a heapdump to /, '') + .replace(/".+$/, ''); + + const didWriteFilePath = didWriteRaw + .replace(/^wrote heapdump out to /, '') + .replace(/".+$/, ''); + + expect(aboutToWriteFilePath).toEqual(didWriteFilePath); + expect(path.dirname(didWriteFilePath)).toBe('/tmp'); + const didWriteFile = path.basename(didWriteFilePath); + const dirContents = await fs.readdir(tmpMountDir); + expect(dirContents).toContain(didWriteFile); + + const { size } = await fs.stat(path.join(tmpMountDir, didWriteFile)); + const oneMegabyte = 1024 * 1024; + // at the time of writing size was observed to be 14-15MB but uncertain if this + // is affected by my test development + expect(size).toBeGreaterThanOrEqual(10 * oneMegabyte); + expect(size).toBeLessThanOrEqual(100 * oneMegabyte); + }); +}); diff --git a/__tests__/server/config/env/runTime.spec.js b/__tests__/server/config/env/runTime.spec.js index ecde8ec0..9ddf2109 100644 --- a/__tests__/server/config/env/runTime.spec.js +++ b/__tests__/server/config/env/runTime.spec.js @@ -34,7 +34,6 @@ describe('runTime', () => { const origEnvVarVals = {}; [ 'NODE_ENV', - 'NODE_HEAPDUMP_OPTIONS', 'HTTP_PORT', 'HTTP_METRICS_PORT', 'HOLOCRON_MODULE_MAP_URL', @@ -126,14 +125,6 @@ describe('runTime', () => { }); }); - describe('NODE_HEAPDUMP_OPTIONS', () => { - const heapdumpOptions = getEnvVarConfig('NODE_HEAPDUMP_OPTIONS'); - - it('defaults to config of not writing default heapdumps', () => { - expect(heapdumpOptions.defaultValue).toBe('nosignal'); - }); - }); - describe('HTTP_PORT', () => { const httpPort = getEnvVarConfig('HTTP_PORT'); diff --git a/__tests__/server/utils/__snapshots__/heapdump.spec.js.snap b/__tests__/server/utils/__snapshots__/heapdump.spec.js.snap new file mode 100644 index 00000000..89af1023 --- /dev/null +++ b/__tests__/server/utils/__snapshots__/heapdump.spec.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`heapdump warns if --report-on-signal is set, using SIGUSR2 1`] = `"--report-on-signal listens for SIGUSR2 by default, be aware that SIGUSR2 also triggers heapdumps. Use --report-signal to avoid heapdump side-effects https://nodejs.org/api/report.html"`; diff --git a/__tests__/server/utils/heapdump.spec.js b/__tests__/server/utils/heapdump.spec.js index db8fc6f4..1d14dfa6 100644 --- a/__tests__/server/utils/heapdump.spec.js +++ b/__tests__/server/utils/heapdump.spec.js @@ -14,72 +14,79 @@ * permissions and limitations under the License. */ -/* eslint import/no-extraneous-dependencies: ["error", { - "devDependencies": false, - "optionalDependencies": true, - "peerDependencies": false -}] */ +const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); -describe('safeRequest', () => { +describe('heapdump', () => { const { pid } = process; - let heapdump; + const mockHeapSnapshotContents = { lots: 'of keys', and: ['many', 'many', 'values'] }; + let v8; + let fs; jest.spyOn(process, 'on').mockImplementation(() => {}); jest.spyOn(Date, 'now').mockImplementation(() => 1525145998246); jest.spyOn(console, 'warn').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); - function load({ heapdumpRequireError = false, heapdumpOptions = 'nosignal' } = {}) { + function load({ mockWriteStreamError = null } = {}) { jest.resetModules(); - if (heapdumpOptions) { - process.env.NODE_HEAPDUMP_OPTIONS = heapdumpOptions; - } else { - delete process.env.NODE_HEAPDUMP_OPTIONS; - } - - jest.doMock('heapdump', () => { - if (heapdumpRequireError) { - throw new Error('unable to resolve heapdump'); - } - return { writeSnapshot: jest.fn() }; - }, { virtual: true }); - - if (!heapdumpRequireError) { - heapdump = require('heapdump'); // eslint-disable-line import/no-unresolved - } else { - heapdump = null; - } + // TODO: do these _need_ to be mocked? or just spied on? + jest.mock('v8', () => ({ + getHeapSnapshot: jest.fn(() => { + const { Readable } = jest.requireActual('stream'); + const heapSnapshot = Buffer.from(JSON.stringify(mockHeapSnapshotContents)); + let pointer = 0; + const readable = new Readable({ + highWaterMark: 20, + read(size) { + const start = pointer; + const end = pointer + size; + const chunk = heapSnapshot.slice(start, end); + this.push(chunk); + pointer = end; + if (pointer >= heapSnapshot.length) { + this.push(null); + } + }, + }); + return readable; + }), + })); + v8 = require('v8'); + + jest.mock('fs', () => ({ + createWriteStream: jest.fn(() => { + const { Writable } = jest.requireActual('stream'); + const mockChunksWritten = []; + let haveEmittedError = false; + const writeStream = new Writable({ + write(chunk, encoding, callback) { + if (mockWriteStreamError) { + if (!haveEmittedError) { + haveEmittedError = true; + setImmediate(() => callback(mockWriteStreamError)); + } + return; + } + mockChunksWritten.push([chunk, encoding]); + setImmediate(callback); + }, + }); + writeStream.mockChunksWritten = mockChunksWritten; + return writeStream; + }), + })); + fs = require('fs'); return require('../../../src/server/utils/heapdump'); } - it('warns when unable to import the heapdump package', () => { - console.warn.mockClear(); - process.on.mockClear(); - load({ heapdumpRequireError: true }); - expect(process.on).not.toHaveBeenCalled(); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn.mock.calls[0][0]).toEqual('unable to setup writing heapdumps'); - }); - - it('warns with the error message when unable to import the heapdump package in development', () => { - console.warn.mockClear(); - process.on.mockClear(); - process.env.NODE_ENV = 'development'; - load({ heapdumpRequireError: true }); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn.mock.calls[0][1]).toEqual('unable to resolve heapdump'); - }); - - it('warns with the error when unable to import the heapdump package in production', () => { - console.warn.mockClear(); - process.on.mockClear(); - process.env.NODE_ENV = 'production'; - load({ heapdumpRequireError: true }); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn.mock.calls[0][1]).toBeInstanceOf(Error); - }); + function waitForStreamToFinish(stream) { + return new Promise((res, rej) => { + stream.on('finish', res); + stream.on('error', rej); + }); + } it('attaches to the SIGUSR2 signal', () => { process.on.mockClear(); @@ -88,61 +95,105 @@ describe('safeRequest', () => { expect(process.on.mock.calls[0][0]).toBe('SIGUSR2'); }); - it('does not attach to the SIGUSR2 signal when options are customized', () => { + it('warns if --report-on-signal is set, using SIGUSR2', () => { + process.execArgv = [ + '--experimental-report', + '--report-on-signal', + ]; console.warn.mockClear(); - process.on.mockClear(); - load({ heapdumpOptions: '' }); - expect(process.on).not.toHaveBeenCalled(); + load(); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn.mock.calls[0][0]).toMatchSnapshot(); }); describe('on SIGUSR2 signal', () => { - it('warns about writing a heapdump', () => { + it('warns about writing a heapdump', async () => { process.on.mockClear(); - console.warn.mockClear(); load(); expect(process.on).toHaveBeenCalledTimes(1); + console.warn.mockClear(); process.on.mock.calls[0][1](); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn.mock.calls[0][0]).toBe(`about to write a heapdump to /tmp/heapdump-${pid}-1525145998246.heapsnapshot`); + await waitForStreamToFinish(fs.createWriteStream.mock.results[0].value); + await sleep(20); // also wait for the callback to run }); - it('attempts to write a heapdump', () => { + it('attempts to get a heap snapshot', async () => { process.on.mockClear(); load(); expect(process.on).toHaveBeenCalledTimes(1); process.on.mock.calls[0][1](); - expect(heapdump.writeSnapshot).toHaveBeenCalledTimes(1); - expect(heapdump.writeSnapshot.mock.calls[0][0]).toBe(`/tmp/heapdump-${pid}-1525145998246.heapsnapshot`); + await waitForStreamToFinish(fs.createWriteStream.mock.results[0].value); + await sleep(20); + expect(v8.getHeapSnapshot).toHaveBeenCalledTimes(1); + expect(v8.getHeapSnapshot).toHaveBeenCalledWith(); }); - describe('writing the heapdump', () => { - const writtenFilename = '/t/m/p/heapsnapshot'; + it('attempts to write a heapdump', async () => { + process.on.mockClear(); + load(); + expect(process.on).toHaveBeenCalledTimes(1); + process.on.mock.calls[0][1](); + await waitForStreamToFinish(fs.createWriteStream.mock.results[0].value); + await sleep(20); + expect(fs.createWriteStream).toHaveBeenCalledTimes(1); + expect(fs.createWriteStream).toHaveBeenCalledWith(`/tmp/heapdump-${pid}-1525145998246.heapsnapshot`); + }); + + it('writes the heapdump to the file', async () => { + expect.assertions(2); + process.on.mockClear(); + load(); + expect(process.on).toHaveBeenCalledTimes(1); + process.on.mock.calls[0][1](); + const sink = fs.createWriteStream.mock.results[0].value; + await waitForStreamToFinish(sink); + await sleep(20); + expect( + JSON.parse( + sink.mockChunksWritten + .map(([chunk]) => chunk.toString('utf8')) + .join('') + ) + ).toEqual(mockHeapSnapshotContents); + }); - it('notifies about an error encountered at level error', () => { + describe('writing the heapdump', () => { + it('notifies about an error encountered at level error', async () => { + expect.assertions(5); process.on.mockClear(); - console.error.mockClear(); - load(); + const mockWriteStreamError = new Error('sample test error'); + load({ mockWriteStreamError }); expect(process.on).toHaveBeenCalledTimes(1); process.on.mock.calls[0][1](); - expect(heapdump.writeSnapshot).toHaveBeenCalledTimes(1); - const err = new Error('sample test error'); - heapdump.writeSnapshot.mock.calls[0][1](err, writtenFilename); + console.error.mockClear(); + console.warn.mockClear(); + const sink = fs.createWriteStream.mock.results[0].value; + await new Promise((res, rej) => { + sink.on('error', res); + sink.on('finish', () => rej(new Error('should have rejected'))); + }); + await sleep(20); // also wait for the callback to run + expect(console.warn).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledTimes(1); - expect(console.error.mock.calls[0][0]).toBe('unable to write heapdump /t/m/p/heapsnapshot'); - expect(console.error.mock.calls[0][1]).toBe(err); + expect(console.error.mock.calls[0][0]).toBe('unable to write heapdump'); + expect(console.error.mock.calls[0][1]).toBe(mockWriteStreamError); }); - it('notifies about finishing at level warn', () => { + it('notifies about finishing at level warn', async () => { + expect.assertions(4); process.on.mockClear(); load(); expect(process.on).toHaveBeenCalledTimes(1); process.on.mock.calls[0][1](); - expect(heapdump.writeSnapshot).toHaveBeenCalledTimes(1); - + console.error.mockClear(); console.warn.mockClear(); - heapdump.writeSnapshot.mock.calls[0][1](null, writtenFilename); + await waitForStreamToFinish(fs.createWriteStream.mock.results[0].value); + await sleep(20); // also wait for the callback to run + expect(console.error).not.toHaveBeenCalled(); expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn.mock.calls[0][0]).toBe('wrote heapdump out to /t/m/p/heapsnapshot'); + expect(console.warn.mock.calls[0][0]).toBe(`wrote heapdump out to /tmp/heapdump-${pid}-1525145998246.heapsnapshot`); }); }); }); diff --git a/package-lock.json b/package-lock.json index 040b210b..b0babc50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7066,7 +7066,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, "optional": true, "requires": { "file-uri-to-path": "1.0.0" @@ -12526,8 +12525,7 @@ "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "fill-range": { "version": "7.0.1", @@ -12782,9 +12780,9 @@ } }, "fs-minipass": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.0.0.tgz", - "integrity": "sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "requires": { "minipass": "^3.0.0" } @@ -13760,15 +13758,6 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, - "heapdump": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz", - "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==", - "optional": true, - "requires": { - "nan": "^2.13.2" - } - }, "helmet": { "version": "3.22.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.22.0.tgz", @@ -20597,7 +20586,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true, "optional": true }, "path-exists": { @@ -21788,8 +21776,7 @@ "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "renderkid": { "version": "2.0.3", @@ -24932,19 +24919,170 @@ "chokidar": "^2.1.8" }, "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "optional": true, "requires": { + "anymatch": "^2.0.0", "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", "upath": "^1.1.1" } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, diff --git a/package.json b/package.json index f6c10c78..7522dee6 100644 --- a/package.json +++ b/package.json @@ -187,9 +187,6 @@ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } }, - "optionalDependencies": { - "heapdump": "^0.3.15" - }, "standard-version": { "bumpFiles": [ "package.json", diff --git a/prod-sample/docker-compose.yml b/prod-sample/docker-compose.yml index 36ef6819..547a6efa 100644 --- a/prod-sample/docker-compose.yml +++ b/prod-sample/docker-compose.yml @@ -21,6 +21,7 @@ services: - './one-app/one-app-privkey.pem:/opt/key.pem' - './nginx/nginx-cert.pem:/opt/nginx-cert.pem' - './extra-certs.pem:/opt/extra-certs.pem' + - './one-app/tmp:/tmp' networks: one-app-at-test-network: depends_on: diff --git a/src/server/config/env/runTime.js b/src/server/config/env/runTime.js index ef7bf682..666f60c1 100644 --- a/src/server/config/env/runTime.js +++ b/src/server/config/env/runTime.js @@ -43,11 +43,6 @@ const runTime = [ valid: ['development', 'production'], defaultValue: 'production', }, - // heapdump config - { - name: 'NODE_HEAPDUMP_OPTIONS', - defaultValue: 'nosignal', - }, // IPv4 port to bind on { name: 'HTTP_PORT', diff --git a/src/server/utils/heapdump.js b/src/server/utils/heapdump.js index 5cbd87cc..0d7b0256 100644 --- a/src/server/utils/heapdump.js +++ b/src/server/utils/heapdump.js @@ -13,29 +13,33 @@ * or implied. See the License for the specific language governing * permissions and limitations under the License. */ +import fs from 'fs'; +import v8 from 'v8'; +import { pipeline } from 'stream'; +import { promisify } from 'util'; -// NODE_HEAPDUMP_OPTIONS=nosignal by default in server/config/env/runTime -let heapdump; -try { - // binary might not be compatible for non-docker distributions - heapdump = require('heapdump'); // eslint-disable-line global-require, import/no-extraneous-dependencies, import/no-unresolved -} catch (err) { +const pipelinePromise = promisify(pipeline); + +// --report-on-signal added which also listens on SIGUSR2 by default +// https://nodejs.org/api/report.html +// if someone is starting node with this option let them know about heapdumps +if (process.execArgv.includes('--report-on-signal')) { console.warn( - 'unable to setup writing heapdumps', - process.env.NODE_ENV === 'development' ? err.message : err + '--report-on-signal listens for SIGUSR2 by default, be aware that SIGUSR2 also triggers heapdumps. Use --report-signal to avoid heapdump side-effects https://nodejs.org/api/report.html' ); } -if (heapdump && process.env.NODE_HEAPDUMP_OPTIONS === 'nosignal') { - process.on('SIGUSR2', () => { - const targetFilename = `/tmp/heapdump-${process.pid}-${Date.now()}.heapsnapshot`; - console.warn(`about to write a heapdump to ${targetFilename}`); - heapdump.writeSnapshot(targetFilename, (err, writtenFilename) => { - if (err) { - console.error(`unable to write heapdump ${writtenFilename}`, err); - } else { - console.warn(`wrote heapdump out to ${writtenFilename}`); - } - }); - }); -} +process.on('SIGUSR2', async () => { + const targetFilename = `/tmp/heapdump-${process.pid}-${Date.now()}.heapsnapshot`; + console.warn(`about to write a heapdump to ${targetFilename}`); + try { + await pipelinePromise( + v8.getHeapSnapshot(), + fs.createWriteStream(targetFilename) + ); + } catch (err) { + console.error('unable to write heapdump', err); + return; + } + console.warn(`wrote heapdump out to ${targetFilename}`); +});