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>
47 lines
1.5 KiB
JavaScript
47 lines
1.5 KiB
JavaScript
// Tests that in watch mode --process-timeout applies to each run of the
|
|
// application, but not to the process that watches for changes.
|
|
|
|
import * as common from '../common/index.mjs';
|
|
import tmpdir from '../common/tmpdir.js';
|
|
import assert from 'node:assert';
|
|
import { spawn } from 'node:child_process';
|
|
import { once } from 'node:events';
|
|
import { writeFileSync } from 'node:fs';
|
|
import { setTimeout } from 'node:timers/promises';
|
|
import { createInterface } from 'node:readline';
|
|
|
|
if (common.isIBMi)
|
|
common.skip('IBMi does not support `fs.watch()`');
|
|
|
|
tmpdir.refresh();
|
|
const file = tmpdir.resolve('hang.js');
|
|
writeFileSync(file, 'setInterval(() => {}, 60_000);');
|
|
|
|
const timeout = common.platformTimeout(500);
|
|
const child = spawn(process.execPath, [
|
|
'--watch',
|
|
'--no-warnings',
|
|
`--process-timeout=${timeout}ms`,
|
|
file,
|
|
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
|
|
let stderr = '';
|
|
child.stderr.setEncoding('utf8');
|
|
child.stderr.on('data', (data) => { stderr += data; });
|
|
|
|
for await (const line of createInterface({ input: child.stdout })) {
|
|
if (line.startsWith('Failed running')) break;
|
|
}
|
|
|
|
// The watcher is still running well after its own deadline would have passed.
|
|
await setTimeout(timeout);
|
|
assert.strictEqual(child.exitCode, null);
|
|
|
|
child.kill();
|
|
await once(child, 'close');
|
|
// What the run was doing depends on how quickly it started, so only check
|
|
// that it timed out.
|
|
assert.match(stderr, new RegExp(
|
|
`^\\(node:\\d+\\) Process timed out after ${timeout}ms \\(--process-timeout\\)\\. ` +
|
|
'Exiting with code 124\\.$', 'm'));
|