fix(process): preserve the cause of cleanup observation failures (#140015)

Retain the original kernel observation error on the existing cleanup error. Keep classification, closure decisions and deadlines unchanged.
This commit is contained in:
Peter Steinberger 2026-09-06 03:46:26 -07:00 • committed by GitHub
parent dc0578ae67
commit c2ffbec6fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 15 additions and 8 deletions

View file

@ -466,11 +466,15 @@ it.each(["EPERM", "EIO", "still present"])(
"keeps graceful cleanup uncertain when the kernel group is %s",
async (failure) => {
const { adapter, completeRoot, emit, close, groupProbe } = await createRelay("linux");
const cause =
failure === "still present"
? undefined
: Object.assign(new Error(`synthetic ${failure}`), { code: failure });
groupProbe.mockImplementation(() => {
if (failure === "still present") {
return true;
if (cause) {
throw cause;
}
throw Object.assign(new Error(`synthetic ${failure}`), { code: failure });
return true;
});
completeRoot();
await expect(adapter.wait()).resolves.toEqual({ code: 0, signal: null });
@ -480,6 +484,9 @@ it.each(["EPERM", "EIO", "still present"])(
vi.spyOn(Date, "now").mockReturnValueOnce(10_000).mockReturnValue(15_000);
close();
await expect(adapter.waitForExtinction()).rejects.toThrow("owned process group");
await expect(adapter.waitForExtinction()).rejects.toSatisfy(
(error: unknown) => error instanceof Error && error.cause === cause,
);
await expect(adapter.wait()).resolves.toEqual({ code: 0, signal: null });
expect(groupProbe).toHaveBeenCalledWith(-1235, 0);
expect(groupProbe.mock.calls.every(([, signal]) => signal === 0)).toBe(true);

View file

@ -255,12 +255,12 @@ export async function createServiceChildRelayAdapter(
child.stderr?.once("end", settleWait);
child.stderr?.once("close", settleWait);
const loseIdentity = (message: string) => {
const loseIdentity = (message: string, options?: ErrorOptions) => {
if (state === "closed" || state === "identity-lost") {
return;
}
state = "identity-lost";
waitError = new Error(`service child cleanup identity lost: ${message}`);
waitError = new Error(`service child cleanup identity lost: ${message}`, options);
events.emitError(waitError, "process");
if (!commandPid) {
startup.reject(waitError);
@ -347,12 +347,12 @@ export async function createServiceChildRelayAdapter(
try {
// Observation only: signalling a retired numeric PGID could hit a reused group.
process.kill(-anchorPid, 0);
} catch (error) {
} catch (cause) {
// SAFETY: process.kill throws Node system errors; only the exact ESRCH code certifies absence.
if ((error as NodeJS.ErrnoException).code === "ESRCH") {
if ((cause as NodeJS.ErrnoException).code === "ESRCH") {
finishAuthorityClose(missingReceiptError);
} else {
loseIdentity("owned process group disappearance could not be confirmed");
loseIdentity("owned process group disappearance could not be confirmed", { cause });
}
return;
}