Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

feat(server): use native V8 heapdump #45

Merged
merged 3 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ prod-sample/nginx/origin-statics
*.key
sample-module-bundles
docker-compose.test.yml
*.heapsnapshot

# misc
.DS_Store
Expand Down
58 changes: 53 additions & 5 deletions __tests__/integration/helpers/testRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));

Expand All @@ -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.
Expand All @@ -84,6 +91,10 @@ const setUpTestRunner = async ({ oneAppLocalPortToUse, oneAppMetricsLocalPortToU
);
}

if (skipBrowser) {
return {};
}

const browser = await remote({
logLevel: 'silent',
protocol: 'http',
Expand All @@ -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(); }

Expand All @@ -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,
};
51 changes: 50 additions & 1 deletion __tests__/integration/one-app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
});
9 changes: 0 additions & 9 deletions __tests__/server/config/env/runTime.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ describe('runTime', () => {
const origEnvVarVals = {};
[
'NODE_ENV',
'NODE_HEAPDUMP_OPTIONS',
'HTTP_PORT',
'HTTP_METRICS_PORT',
'HOLOCRON_MODULE_MAP_URL',
Expand Down Expand Up @@ -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');

Expand Down
3 changes: 3 additions & 0 deletions __tests__/server/utils/__snapshots__/heapdump.spec.js.snap
Original file line number Diff line number Diff line change
@@ -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"`;
Loading