Files
node/test/parallel/test-process-timeout-blocked.js
Trivikram Kamat e21f81228d src: fix --process-timeout message when exit waits
process.exit(), uncaught exceptions and unhandled rejections exit
through Environment::Exit(), which never calls OnEnvironmentStopping().
If a Worker blocked in a synchronous native call held up that exit past
the deadline, the watchdog blamed the main thread.

Check Environment::is_stopping() and report that the process did not
finish exiting instead.

Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com>
Assisted-by: claude:opus-5.5
PR-URL: https://github.com/nodejs/node/pull/66361
Fixes: https://github.com/nodejs/node/issues/66360
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
2026-10-03 09:51:01 +00:00

84 lines
3.2 KiB
JavaScript

'use strict';
// Tests that --process-timeout makes the process exit even if the main thread
// cannot be interrupted.
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const { spawnSync } = require('child_process');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
// The deadline is measured from the start of the process, so on a slow machine
// it can expire before the fixture has reached the state under test, which the
// fixture signals by creating a file. If it did not get that far and did not
// print the expected message, run it again with a longer timeout.
function runUntilReady(fixture, expected, args = []) {
for (let timeout = common.platformTimeout(1000); ; timeout *= 2) {
// Child processes of a previous attempt may still be running, so use a
// different file for each attempt.
const marker = tmpdir.resolve([fixture, ...args, timeout, 'ready'].join('.'));
const start = process.hrtime.bigint();
const child = spawnSync(process.execPath, [
`--process-timeout=${timeout}ms`,
fixtures.path('process-timeout', fixture),
marker,
...args,
], { encoding: 'utf8' });
const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
if (!expected.test(child.stderr) && !fs.existsSync(marker) &&
timeout < common.platformTimeout(16000)) {
continue;
}
assert.strictEqual(child.signal, null);
assert.strictEqual(child.status, 124, child.stderr);
assert.ok(elapsed >= timeout, `Exited after ${elapsed}ms, before ${timeout}ms`);
assert.match(child.stderr, new RegExp(
`^\\(node:\\d+\\) Process timed out after ${timeout}ms \\(--process-timeout\\)\\. ` +
'Exiting with code 124\\.$', 'm'));
assert.match(child.stderr, expected);
return child.stderr;
}
}
{
// The main thread is blocked in a synchronous native call.
const stderr = runUntilReady(
'blocked-main-thread.js',
/^The main thread did not respond within 2000ms\. It is likely blocked/m);
assert.doesNotMatch(stderr, /Main thread was/);
}
{
// The main thread responds, but exiting waits for a Worker thread that is
// blocked in a synchronous native call.
const stderr = runUntilReady(
'blocked-worker.js',
/^\(node:\d+\) The process did not finish exiting within 5000ms after --process-timeout expired\. Forcing exit\.$/m);
assert.match(stderr, /^ {4}Worker \(thread 1, name 'blocked'\)$/m);
}
{
// The event loop has stopped, but exiting waits for a Worker thread that is
// blocked in a synchronous native call.
runUntilReady(
'blocked-worker-at-exit.js',
/^The process did not finish exiting after the event loop had stopped\.$/m);
}
for (const mode of ['exit', 'throw', 'reject']) {
// process.exit(), an uncaught exception, or an unhandled rejection waits for
// a Worker thread that is blocked in a synchronous native call. The main thread is exiting, so it
// must not be reported as blocked by the application.
const stderr = runUntilReady(
'blocked-worker-at-process-exit.js',
/^The process did not finish exiting after the event loop had stopped\.$/m,
[mode]);
assert.doesNotMatch(stderr, /The main thread did not respond/);
}