mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Putting a time limit on a Node.js process is common in many
use cases, but each of the common approaches are generally
flawed.
OS and CI timeouts are inconsistent and blind. Every OS flavor
handles process timeouts in different ways. Stock macOS and
Windows do not ship coreutils `timeout`, etc. None of the options
can explain why the process was still running or what exactly
was interrupted. Using setTimeout only fires when the event loop
is free so it misses busy loops and thread blocks and cannot
see the stack. Diagnostics triggered by signal have the same
problem.
This introduces a `--process-timeout` that should work
consistently across all runtimes. A native watchdog thread
interrupts the main thread, even in the middle of running
code. It prints the JavaScript stack (if any) and resources
keeping the event loop alive (if any), and can optionally
print a full diagnostic report. The process exits with a
distinct code (124). If the main thread is stuck in native
code, the process still exits.
$ node --process-timeout=10s -e "while(true) {}"
(node:483970) Process timed out after 10s (--process-timeout). Exiting with code 124.
Main thread was executing JavaScript:
at [eval]:1:1
at runScriptInThisContext (node:internal/vm:219:10)
at node:internal/process/execution:485:12
at [eval]-wrapper:6:24
at runScriptInContext (node:internal/process/execution:483:60)
at evalFunction (node:internal/process/execution:317:30)
at evalTypeScript (node:internal/process/execution:329:3)
at node:internal/main/eval_string:71:3
No resources keeping the event loop alive were found.
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode
PR-URL: https://github.com/nodejs/node/pull/66138
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
119 lines
3.6 KiB
JavaScript
119 lines
3.6 KiB
JavaScript
'use strict';
|
|
|
|
// Tests the validation of --process-timeout and of the options that cannot be
|
|
// combined with it.
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const { Worker } = require('worker_threads');
|
|
const {
|
|
spawnSyncAndAssert,
|
|
spawnSyncAndExit,
|
|
} = require('../common/child_process');
|
|
|
|
// Long enough for the process to exit on its own.
|
|
for (const args of [
|
|
['--process-timeout=600000ms'],
|
|
['--process-timeout=30s'],
|
|
['--process-timeout=5m'],
|
|
['--process-timeout', '1h'],
|
|
]) {
|
|
spawnSyncAndAssert(process.execPath, [...args, '-e', ''], {
|
|
stdout: '',
|
|
stderr: '',
|
|
});
|
|
}
|
|
|
|
for (const value of [
|
|
'30', '0s', '0ms', '1.5s', '-1s', '1d', 's', 'abc', ' 1s', '1s ',
|
|
'99999999999999999999h',
|
|
]) {
|
|
spawnSyncAndExit(process.execPath, [`--process-timeout=${value}`, '-e', ''], {
|
|
status: 9,
|
|
signal: null,
|
|
trim: true,
|
|
stderr: `${process.execPath}: invalid value for --process-timeout: ` +
|
|
`'${value}'. Expected a positive integer followed by a unit ` +
|
|
'(ms, s, m or h), e.g. 30s',
|
|
});
|
|
}
|
|
|
|
spawnSyncAndExit(process.execPath, ['--report-on-process-timeout', '-e', ''], {
|
|
status: 9,
|
|
signal: null,
|
|
trim: true,
|
|
stderr: `${process.execPath}: --report-on-process-timeout must be used with ` +
|
|
'--process-timeout',
|
|
});
|
|
|
|
if (!process.config.variables.node_without_node_options) {
|
|
for (const option of ['--process-timeout=1s', '--report-on-process-timeout']) {
|
|
spawnSyncAndExit(process.execPath, ['-e', ''], {
|
|
env: { ...process.env, NODE_OPTIONS: option },
|
|
}, {
|
|
status: 9,
|
|
signal: null,
|
|
trim: true,
|
|
stderr: `${process.execPath}: ${option.replace(/=.*/, '=')} is not ` +
|
|
'allowed in NODE_OPTIONS',
|
|
});
|
|
}
|
|
|
|
// Conflicts are detected across option sources. The inspector options are
|
|
// the only conflicting options allowed in NODE_OPTIONS, and they only exist
|
|
// when Node.js is built with the inspector.
|
|
if (common.hasInspector) {
|
|
spawnSyncAndExit(process.execPath, ['--process-timeout=1s', '-e', ''], {
|
|
env: { ...process.env, NODE_OPTIONS: '--inspect=0' },
|
|
}, {
|
|
status: 9,
|
|
signal: null,
|
|
trim: true,
|
|
stderr: `${process.execPath}: either --process-timeout or --inspect can be ` +
|
|
'used, not both',
|
|
});
|
|
}
|
|
}
|
|
|
|
const conflicts = [
|
|
[['--run', 'test'], '--run'],
|
|
[['--build-snapshot', 'entry.js'], '--build-snapshot'],
|
|
];
|
|
// Without the inspector, these options are rejected as bad options instead.
|
|
if (common.hasInspector) {
|
|
conflicts.push(
|
|
[['--inspect=0'], '--inspect'],
|
|
[['--inspect-brk=0'], '--inspect-brk'],
|
|
[['--inspect-brk-node=0'], '--inspect-brk-node'],
|
|
[['--inspect-wait=0'], '--inspect-wait'],
|
|
[['--inspect-port=0'], '--inspect-port'],
|
|
[['--debug-port=0'], '--inspect-port'],
|
|
[['--inspect-publish-uid=http'], '--inspect-publish-uid'],
|
|
);
|
|
}
|
|
|
|
for (const [args, conflict] of conflicts) {
|
|
spawnSyncAndExit(process.execPath, ['--process-timeout=1s', ...args], {
|
|
status: 9,
|
|
signal: null,
|
|
trim: true,
|
|
stderr: `${process.execPath}: either --process-timeout or ${conflict} can ` +
|
|
'be used, not both',
|
|
});
|
|
}
|
|
|
|
spawnSyncAndExit(process.execPath, ['--process-timeout=1s', 'inspect', 'app.js'], {
|
|
status: 9,
|
|
signal: null,
|
|
trim: true,
|
|
stderr: `${process.execPath}: --process-timeout cannot be used with ` +
|
|
'`node inspect`',
|
|
});
|
|
|
|
// --process-timeout applies to the whole process, so Workers do not accept it.
|
|
assert.throws(() => {
|
|
new Worker('', { eval: true, execArgv: ['--process-timeout=1s'] });
|
|
}, {
|
|
code: 'ERR_WORKER_INVALID_EXEC_ARGV',
|
|
});
|