Skip to content

Commit 7f3883b

Browse files
committed
test(auto-instrumentations-node): reproduce blocking process termination
This adds a (currently failing) test to verify that the automatic SDK shutdown at process end does not inadvertently block process termination for too long.
1 parent d14f8e5 commit 7f3883b

File tree

1 file changed

+75
-15
lines changed

1 file changed

+75
-15
lines changed

packages/auto-instrumentations-node/test/register.test.ts

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,46 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { execFile, PromiseWithChild } from 'child_process';
17+
import { execFile, PromiseWithChild, ChildProcess } from 'child_process';
18+
import { once } from 'events';
1819
import * as assert from 'assert';
1920
import { promisify } from 'util';
2021
import { Readable } from 'stream';
2122

2223
const execFilePromise = promisify(execFile);
24+
const appTerminationThreshold = 500;
2325

24-
function runWithRegister(path: string): PromiseWithChild<{
26+
function runWithRegister(
27+
path: string,
28+
envOverrides?: any
29+
): PromiseWithChild<{
2530
stdout: string;
2631
stderr: string;
2732
}> {
33+
let env = Object.assign({}, process.env, {
34+
OTEL_TRACES_EXPORTER: 'console',
35+
OTEL_METRICS_EXPORTER: 'none',
36+
OTEL_LOG_LEVEL: 'debug',
37+
// nx (used by lerna run) defaults `FORCE_COLOR=true`, which in
38+
// node v18.17.0, v20.3.0 and later results in ANSI color escapes
39+
// in the ConsoleSpanExporter output that is checked below.
40+
FORCE_COLOR: '0',
41+
// Cloud resource detectors can take a few seconds, resulting in hitting
42+
// a test timeout.
43+
OTEL_NODE_RESOURCE_DETECTORS: 'none',
44+
});
45+
if (envOverrides) {
46+
env = Object.assign(env, envOverrides);
47+
}
48+
2849
return execFilePromise(
2950
process.execPath,
3051
['--require', '../build/src/register.js', path],
3152
{
3253
cwd: __dirname,
3354
timeout: 5000,
3455
killSignal: 'SIGKILL', // SIGTERM is not sufficient to terminate some hangs
35-
env: Object.assign({}, process.env, {
36-
OTEL_TRACES_EXPORTER: 'console',
37-
OTEL_METRICS_EXPORTER: 'none',
38-
OTEL_LOG_LEVEL: 'debug',
39-
// nx (used by lerna run) defaults `FORCE_COLOR=true`, which in
40-
// node v18.17.0, v20.3.0 and later results in ANSI color escapes
41-
// in the ConsoleSpanExporter output that is checked below.
42-
FORCE_COLOR: '0',
43-
// Cloud resource detectors can take a few seconds, resulting in hitting
44-
// a test timeout.
45-
OTEL_NODE_RESOURCE_DETECTORS: 'none',
46-
}),
56+
env,
4757
}
4858
);
4959
}
@@ -63,8 +73,17 @@ function waitForString(stream: Readable, str: string): Promise<void> {
6373
});
6474
}
6575

76+
function registerExitListeners(child: ChildProcess) {
77+
return [
78+
once(child, 'exit'),
79+
once(child, 'error').then(null, err => Promise.reject(err)),
80+
];
81+
}
82+
6683
describe('Register', function () {
67-
this.timeout(5000); // Same timeout as in the execFile() above.
84+
// Deliberately use a timeout > 5000 ms, so that failures in 'automatic NodeSDK shutdown should not block process termination'
85+
// result in a test failure and not in a test timeout.
86+
this.timeout(10000);
6887

6988
it('can load auto instrumentation from command line', async () => {
7089
const runPromise = runWithRegister('./test-app/app.js');
@@ -95,7 +114,14 @@ describe('Register', function () {
95114
it('shuts down the NodeSDK when SIGTERM is received', async () => {
96115
const runPromise = runWithRegister('./test-app/app-server.js');
97116
const { child } = runPromise;
117+
const [exitPromise, errorPromise] = registerExitListeners(child);
98118
await waitForString(child.stdout!, 'Finished request');
119+
120+
let appTerminationTook = 0;
121+
child.on('exit', () => {
122+
appTerminationTook = Date.now() - sigtermSentAt;
123+
});
124+
const sigtermSentAt = Date.now();
99125
child.kill('SIGTERM');
100126
const { stdout } = await runPromise;
101127

@@ -106,5 +132,39 @@ describe('Register', function () {
106132

107133
// Check a span has been generated for the GET request done in app.js
108134
assert.ok(stdout.includes("name: 'GET'"), 'console span output in stdout');
135+
136+
// Verify that the SDK shutdown does not block termination of the process.
137+
await Promise.race([exitPromise, errorPromise]);
138+
assert.ok(
139+
appTerminationTook < appTerminationThreshold,
140+
`The application under test terminated ${appTerminationTook} ms after sending SIGTERM, exceeding the allowed threshold of ${appTerminationThreshold} ms.`
141+
);
142+
});
143+
144+
it('does not block process termination', async () => {
145+
const runPromise = runWithRegister('./test-app/app-server.js', {
146+
// Override the console exporter which are used in the other tests with an otlp exporter, to include DNS lookup,
147+
// establishing a connection etc.
148+
OTEL_TRACES_EXPORTER: 'otlp',
149+
OTEL_EXPORTER_OTLP_ENDPOINT: 'https://example.com:4317',
150+
// 'ingress.eu-west-1.aws.dash0-dev.com:4317',
151+
});
152+
const { child } = runPromise;
153+
const [exitPromise, errorPromise] = registerExitListeners(child);
154+
await waitForString(child.stdout!, 'Finished request');
155+
156+
let appTerminationTook = 0;
157+
child.on('exit', () => {
158+
appTerminationTook = Date.now() - sigtermSentAt;
159+
});
160+
const sigtermSentAt = Date.now();
161+
child.kill('SIGTERM');
162+
163+
// Verify that the SDK shutdown does not block termination of the process.
164+
await Promise.race([exitPromise, errorPromise]);
165+
assert.ok(
166+
appTerminationTook < appTerminationThreshold,
167+
`The application under test terminated ${appTerminationTook} ms after sending SIGTERM, exceeding the allowed threshold of ${appTerminationThreshold} ms.`
168+
);
109169
});
110170
});

0 commit comments

Comments
 (0)