pi-mono/packages/chord/test/node/workers/main.test.ts
Armin Ronacher 7aed50ae8b fix(chord): clear stale worker lock files on macOS and the BSDs
A worker killed by a signal left its lock's socket file, and every later start of its address failed closed on it, retry too. The workers component now removes a lock socket file nobody accepts on before each start and after a child's exit by a signal, in the address's queue. processLock also treats ENOTSOCK as stale instead of retrying until its context ended.
2026-11-03 17:48:14 +01:00

89 lines
3.7 KiB
TypeScript

// Tests of the worker's side (slice 10 §3): the start message, the lock, `started` on `connect`, `ready`; shutdown
// during startup, the parent's disconnect and SIGTERM each run the closers and exit; `fatal` redacted.
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { hostKind, str } from "../../../src/contracts/index.ts";
import type { FromWorker } from "../../../src/node/workers/control.ts";
import { type Exit, WorkerProcess } from "../../../src/node/workers/process.ts";
import type { WorkerBehaviour } from "../fixtures/workers/control.ts";
import { until } from "./support.ts";
const ENTRY = fileURLToPath(new URL("../fixtures/workers/worker.ts", import.meta.url));
const Session = hostKind("session", { id: str() });
/** The fixture worker forked and started with `behaviour`; its control messages and exit collected. */
function started(behaviour: WorkerBehaviour) {
const control: FromWorker[] = [];
let exit: Exit | undefined;
const child = new WorkerProcess(
{
entry: ENTRY,
execArgv: ["--conditions=source", "--experimental-strip-types", "--no-warnings"],
onOutput: () => {},
},
{
control: (message) => control.push(message),
linkLost: () => {},
linkClosed: () => {},
exited: (value) => {
exit = value;
},
},
);
child.send({
kind: "start",
address: Session({ id: "S1" }),
start: behaviour,
scripts: [],
lock: `chord-test-main:${process.pid}:${Date.now()}:${Math.random()}`,
});
return { child, control, exited: () => exit };
}
describe("the worker's side", () => {
it("sends started when the app connects, then ready with its load report and idle state", async () => {
const { child, control } = started({});
await until(() => control.some(({ kind }) => kind === "ready"));
expect(control.map(({ kind }) => kind)).toEqual(["started", "ready"]);
expect(control[1]).toMatchObject({ idle: true, report: { plugins: { added: [] }, failed: [] } });
// Shut down, not killed: a SIGKILL would leave its lock's socket file on macOS and the BSDs.
child.send({ kind: "shutdown" });
await child.exited;
});
it("shuts down during its startup: aborts it, runs the closers registered so far in reverse order, exits 0", async () => {
const dir = await mkdtemp(join(tmpdir(), "chord-main-"));
const { child, control, exited } = started({ hang: true, log: join(dir, "log") });
await until(() => control.some(({ kind }) => kind === "started"));
child.send({ kind: "shutdown" });
await until(() => exited() !== undefined);
expect(exited()).toEqual({ code: 0, signal: null });
expect(await readFile(join(dir, "log"), "utf8")).toBe("store\nhost\n");
await rm(dir, { recursive: true, force: true });
});
it("shuts down on SIGTERM and on the parent's disconnect", async () => {
const term = started({});
await until(() => term.control.some(({ kind }) => kind === "ready"));
process.kill(term.child.pid, "SIGTERM");
await until(() => term.exited() !== undefined);
expect(term.exited()).toEqual({ code: 0, signal: null });
const gone = started({});
await until(() => gone.control.some(({ kind }) => kind === "ready"));
gone.child.link.close();
await until(() => gone.exited() !== undefined);
expect(gone.exited()).toEqual({ code: 0, signal: null });
});
it("sends fatal with the redacted error and exits 1", async () => {
const { control, exited } = started({ fatal: "no secret-key-42 here" });
await until(() => exited() !== undefined);
expect(control).toEqual([{ kind: "fatal", error: { message: "no [redacted] here" } }]);
expect(exited()?.code).toBe(1);
});
});