From ded3a930587638d4014442db133e6d2cb40ab279 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Fri, 12 Jun 2026 18:52:34 -0700 Subject: [PATCH] fix(e2e): keep lifecycle timeout cleanup alive (#92566) --- .../lib/plugin-lifecycle-matrix/measure.mjs | 30 ++++++++++++++++++- test/scripts/plugin-lifecycle-measure.test.ts | 6 +++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs b/scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs index 6b2750436dea..3cdd1d8e984d 100644 --- a/scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs +++ b/scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs @@ -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); }); diff --git a/test/scripts/plugin-lifecycle-measure.test.ts b/test/scripts/plugin-lifecycle-measure.test.ts index 79989a85333a..1d21752d63c2 100644 --- a/test/scripts/plugin-lifecycle-measure.test.ts +++ b/test/scripts/plugin-lifecycle-measure.test.ts @@ -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(),