fix(e2e): keep lifecycle timeout cleanup alive (#92566)

This commit is contained in:
Dallin Romney
2026-06-12 18:52:34 -07:00
committed by GitHub
parent 0063f3076c
commit ded3a93058
2 changed files with 34 additions and 2 deletions
@@ -72,11 +72,13 @@ function readProcSnapshot() {
.trim()
.split(/\s+/u);
const ppid = Number.parseInt(fields[1] ?? "", 10);
const pgrp = Number.parseInt(fields[2] ?? "", 10);
const userTicks = Number.parseInt(fields[11] ?? "", 10);
const systemTicks = Number.parseInt(fields[12] ?? "", 10);
const rssPages = Number.parseInt(fields[21] ?? "", 10);
if (
!Number.isFinite(ppid) ||
!Number.isFinite(pgrp) ||
!Number.isFinite(userTicks) ||
!Number.isFinite(systemTicks) ||
!Number.isFinite(rssPages)
@@ -85,6 +87,7 @@ function readProcSnapshot() {
}
stats.set(pid, {
ppid,
pgrp,
cpuTicks: userTicks + systemTicks,
rssBytes: Math.max(0, rssPages) * pageSize,
});
@@ -117,7 +120,10 @@ function descendantsOf(rootPid, stats) {
function sample(rootPid) {
const stats = readProcSnapshot();
const pids = descendantsOf(rootPid, stats);
const groupPids = new Set(
[...stats.entries()].filter(([, stat]) => stat.pgrp === rootPid).map(([pid]) => pid),
);
const pids = new Set([...descendantsOf(rootPid, stats), ...groupPids]);
let rssBytes = 0;
let cpuTicks = 0;
for (const pid of pids) {
@@ -148,6 +154,10 @@ let forwardedParentSignal = null;
let killTimer;
let parentSignalTimer;
let parentSignalPollTimer;
let childGroupDrainTimer;
// The leader can exit before descendants in its detached process group.
// Keep the wrapper alive so timeout cleanup still owns those descendants.
let childClosedResult = null;
const updateMetrics = () => {
if (!child.pid) {
return;
@@ -157,11 +167,21 @@ const updateMetrics = () => {
maxCpuTicks = Math.max(maxCpuTicks, current.cpuTicks);
};
function finishChildClosedResultIfGroupDrained() {
if (childClosedResult && !childGroupExists()) {
finish(childClosedResult.code, childClosedResult.signal);
}
}
updateMetrics();
const interval = setInterval(updateMetrics, pollMs);
const timeoutTimer =
Number.isFinite(timeoutMs) && timeoutMs > 0
? setTimeout(() => {
if (childClosedResult && !childGroupExists()) {
finish(childClosedResult.code, childClosedResult.signal);
return;
}
timedOut = true;
terminateChildGroup("SIGTERM");
killTimer = setTimeout(() => {
@@ -215,6 +235,9 @@ function clearRuntimeTimers() {
if (parentSignalPollTimer) {
clearInterval(parentSignalPollTimer);
}
if (childGroupDrainTimer) {
clearInterval(childGroupDrainTimer);
}
}
function rethrowParentSignal(signal) {
@@ -329,5 +352,10 @@ child.on("exit", (code, signal) => {
if (timedOut && killTimer) {
return;
}
if (childGroupExists()) {
childClosedResult = { code, signal };
childGroupDrainTimer = setInterval(finishChildClosedResultIfGroupDrained, Math.min(25, pollMs));
return;
}
finish(code, signal);
});
@@ -220,7 +220,11 @@ describe("plugin lifecycle resource sampler", () => {
"--",
"bash",
"-lc",
'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' & wait',
[
'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' &',
'while [ ! -s "$PID_FILE" ]; do sleep 0.01; done',
"exit 0",
].join("\n"),
],
{
cwd: process.cwd(),