/* oxlint-disable executor/no-try-catch-or-throw -- boundary: subprocess ownership harness must clean up the held lock process even when assertions fail */import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; interface ChildOutput { readonly child: ChildProcessWithoutNullStreams; stdout: string; stderr: string;} interface ChildCloseResult { readonly timedOut: boolean; readonly exitCode: number | null; readonly signalCode: NodeJS.Signals | null; readonly stdout: string; readonly stderr: string;} interface MarkerWaitResult { readonly markerFound: boolean; readonly exitCode: number | null; readonly signalCode: NodeJS.Signals | null; readonly stdout: string; readonly stderr: string;} const appRoot = join(import.meta.dirname, "../..");const LOCK_DATABASE_FILENAME = "data.db.owner-lock"; const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)); const holderScript = ` import { writeFileSync } from "node:fs"; import { setTimeout as sleep } from "node:timers/promises"; import { acquireDataDirOwnership } from "./src/db/data-dir-ownership.ts"; const ownership = await acquireDataDirOwnership(process.env.EXECUTOR_TEST_DATA_DIR); writeFileSync(process.env.EXECUTOR_TEST_READY_MARKER, "ready"); await sleep(60_000); await ownership.release();`; const attemptScript = ` import { DataDirOwnershipHeld, acquireDataDirOwnership } from "./src/db/data-dir-ownership.ts"; try { const ownership = await acquireDataDirOwnership(process.env.EXECUTOR_TEST_DATA_DIR); console.log(\`ACQUIRED \${ownership.lockPath}\`); await ownership.release(); process.exit(0); } catch (cause) { if (cause instanceof DataDirOwnershipHeld) { console.log(\`HELD \${cause.lockPath}\`); process.exit(0); } console.log("FAILED"); console.error(cause); process.exit(1); }`; const spawnScript = (code: string, env: Record<string, string>): ChildOutput => { const child = spawn(process.execPath, ["-e", code], { cwd: appRoot, env: { ...process.env, NODE_ENV: "test", ...env, }, stdio: "pipe", }); const output: ChildOutput = { child, stdout: "", stderr: "" }; child.stdout.setEncoding("utf-8"); child.stderr.setEncoding("utf-8"); child.stdout.on("data", (chunk: string) => { output.stdout += chunk; }); child.stderr.on("data", (chunk: string) => { output.stderr += chunk; }); return output;}; const waitForChildClose = async ( output: ChildOutput, timeoutMs: number,): Promise<ChildCloseResult> => new Promise((resolve) => { const complete = (timedOut: boolean) => { clearTimeout(timer); output.child.off("close", onClose); resolve({ timedOut, exitCode: output.child.exitCode, signalCode: output.child.signalCode, stdout: output.stdout.trim(), stderr: output.stderr.trim(), }); }; const onClose = () => complete(false); const timer = setTimeout(() => complete(true), timeoutMs); if (output.child.exitCode !== null || output.child.signalCode !== null) { complete(false); return; } output.child.once("close", onClose); }); const waitForMarker = async ( markerPath: string, output: ChildOutput, timeoutMs: number,): Promise<MarkerWaitResult> => { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (existsSync(markerPath)) { return { markerFound: true, exitCode: output.child.exitCode, signalCode: output.child.signalCode, stdout: output.stdout.trim(), stderr: output.stderr.trim(), }; } if (output.child.exitCode !== null || output.child.signalCode !== null) { return { markerFound: false, exitCode: output.child.exitCode, signalCode: output.child.signalCode, stdout: output.stdout.trim(), stderr: output.stderr.trim(), }; } await delay(25); } return { markerFound: false, exitCode: output.child.exitCode, signalCode: output.child.signalCode, stdout: output.stdout.trim(), stderr: output.stderr.trim(), };}; const killChild = async (output: ChildOutput): Promise<ChildCloseResult> => { if (output.child.exitCode === null && output.child.signalCode === null) { output.child.kill("SIGKILL"); } return waitForChildClose(output, 5_000);}; const disposeChildOutput = (output: ChildOutput): void => { output.child.stdout.destroy(); output.child.stderr.destroy();}; const runAcquireAttempt = async (dataDir: string): Promise<ChildCloseResult> => { const attempt = spawnScript(attemptScript, { EXECUTOR_TEST_DATA_DIR: dataDir }); const result = await waitForChildClose(attempt, 5_000); disposeChildOutput(attempt); return result;}; describe("data-dir ownership", () => { it("blocks a second process and releases ownership when the holder is SIGKILLed", async () => { const workDir = mkdtempSync(join(tmpdir(), "executor-data-dir-ownership-")); const dataDir = join(workDir, "data # owned %3F"); const marker = join(workDir, "owner-ready"); const holder = spawnScript(holderScript, { EXECUTOR_TEST_DATA_DIR: dataDir, EXECUTOR_TEST_READY_MARKER: marker, }); try { const ready = await waitForMarker(marker, holder, 10_000); expect(ready).toMatchObject({ markerFound: true, exitCode: null, signalCode: null, stdout: "", stderr: "", }); const expectedLockPath = join(realpathSync(dataDir), LOCK_DATABASE_FILENAME); const blocked = await runAcquireAttempt(dataDir); expect(blocked).toMatchObject({ timedOut: false, exitCode: 0, signalCode: null, stdout: `HELD ${expectedLockPath}`, stderr: "", }); const killed = await killChild(holder); expect(killed).toMatchObject({ timedOut: false, exitCode: null, signalCode: "SIGKILL", }); const acquiredAfterKill = await runAcquireAttempt(dataDir); expect(acquiredAfterKill).toMatchObject({ timedOut: false, exitCode: 0, signalCode: null, stdout: `ACQUIRED ${expectedLockPath}`, stderr: "", }); } finally { await killChild(holder); disposeChildOutput(holder); rmSync(workDir, { recursive: true, force: true }); } });});
/* oxlint-disable executor/no-try-catch-or-throw -- boundary: subprocess ownership harness must clean up the held lock process even when assertions fail */
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "@effect/vitest";
interface ChildOutput {
readonly child: ChildProcessWithoutNullStreams;
stdout: string;
stderr: string;
}
interface ChildCloseResult {
readonly timedOut: boolean;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly stdout: string;
readonly stderr: string;
}
interface MarkerWaitResult {
readonly markerFound: boolean;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly stdout: string;
readonly stderr: string;
}
const appRoot = join(import.meta.dirname, "../..");
const LOCK_DATABASE_FILENAME = "data.db.owner-lock";
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
const holderScript = `
import { writeFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
import { acquireDataDirOwnership } from "./src/db/data-dir-ownership.ts";
const ownership = await acquireDataDirOwnership(process.env.EXECUTOR_TEST_DATA_DIR);
writeFileSync(process.env.EXECUTOR_TEST_READY_MARKER, "ready");
await sleep(60_000);
await ownership.release();
`;
const attemptScript = `
import { DataDirOwnershipHeld, acquireDataDirOwnership } from "./src/db/data-dir-ownership.ts";
try {
const ownership = await acquireDataDirOwnership(process.env.EXECUTOR_TEST_DATA_DIR);
console.log(\`ACQUIRED \${ownership.lockPath}\`);
await ownership.release();
process.exit(0);
} catch (cause) {
if (cause instanceof DataDirOwnershipHeld) {
console.log(\`HELD \${cause.lockPath}\`);
process.exit(0);
}
console.log("FAILED");
console.error(cause);
process.exit(1);
}
`;
const spawnScript = (code: string, env: Record<string, string>): ChildOutput => {
const child = spawn(process.execPath, ["-e", code], {
cwd: appRoot,
env: {
...process.env,
NODE_ENV: "test",
...env,
},
stdio: "pipe",
});
const output: ChildOutput = { child, stdout: "", stderr: "" };
child.stdout.setEncoding("utf-8");
child.stderr.setEncoding("utf-8");
child.stdout.on("data", (chunk: string) => {
output.stdout += chunk;
});
child.stderr.on("data", (chunk: string) => {
output.stderr += chunk;
});
return output;
};
const waitForChildClose = async (
output: ChildOutput,
timeoutMs: number,
): Promise<ChildCloseResult> =>
new Promise((resolve) => {
const complete = (timedOut: boolean) => {
clearTimeout(timer);
output.child.off("close", onClose);
resolve({
timedOut,
exitCode: output.child.exitCode,
signalCode: output.child.signalCode,
stdout: output.stdout.trim(),
stderr: output.stderr.trim(),
});
};
const onClose = () => complete(false);
const timer = setTimeout(() => complete(true), timeoutMs);
if (output.child.exitCode !== null || output.child.signalCode !== null) {
complete(false);
return;
}
output.child.once("close", onClose);
});
const waitForMarker = async (
markerPath: string,
output: ChildOutput,
timeoutMs: number,
): Promise<MarkerWaitResult> => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (existsSync(markerPath)) {
return {
markerFound: true,
exitCode: output.child.exitCode,
signalCode: output.child.signalCode,
stdout: output.stdout.trim(),
stderr: output.stderr.trim(),
};
}
if (output.child.exitCode !== null || output.child.signalCode !== null) {
return {
markerFound: false,
exitCode: output.child.exitCode,
signalCode: output.child.signalCode,
stdout: output.stdout.trim(),
stderr: output.stderr.trim(),
};
}
await delay(25);
}
return {
markerFound: false,
exitCode: output.child.exitCode,
signalCode: output.child.signalCode,
stdout: output.stdout.trim(),
stderr: output.stderr.trim(),
};
};
const killChild = async (output: ChildOutput): Promise<ChildCloseResult> => {
if (output.child.exitCode === null && output.child.signalCode === null) {
output.child.kill("SIGKILL");
}
return waitForChildClose(output, 5_000);
};
const disposeChildOutput = (output: ChildOutput): void => {
output.child.stdout.destroy();
output.child.stderr.destroy();
};
const runAcquireAttempt = async (dataDir: string): Promise<ChildCloseResult> => {
const attempt = spawnScript(attemptScript, { EXECUTOR_TEST_DATA_DIR: dataDir });
const result = await waitForChildClose(attempt, 5_000);
disposeChildOutput(attempt);
return result;
};
describe("data-dir ownership", () => {
it("blocks a second process and releases ownership when the holder is SIGKILLed", async () => {
const workDir = mkdtempSync(join(tmpdir(), "executor-data-dir-ownership-"));
const dataDir = join(workDir, "data # owned %3F");
const marker = join(workDir, "owner-ready");
const holder = spawnScript(holderScript, {
EXECUTOR_TEST_DATA_DIR: dataDir,
EXECUTOR_TEST_READY_MARKER: marker,
});
try {
const ready = await waitForMarker(marker, holder, 10_000);
expect(ready).toMatchObject({
markerFound: true,
exitCode: null,
signalCode: null,
stdout: "",
stderr: "",
});
const expectedLockPath = join(realpathSync(dataDir), LOCK_DATABASE_FILENAME);
const blocked = await runAcquireAttempt(dataDir);
expect(blocked).toMatchObject({
timedOut: false,
exitCode: 0,
signalCode: null,
stdout: `HELD ${expectedLockPath}`,
stderr: "",
});
const killed = await killChild(holder);
expect(killed).toMatchObject({
timedOut: false,
exitCode: null,
signalCode: "SIGKILL",
});
const acquiredAfterKill = await runAcquireAttempt(dataDir);
expect(acquiredAfterKill).toMatchObject({
timedOut: false,
exitCode: 0,
signalCode: null,
stdout: `ACQUIRED ${expectedLockPath}`,
stderr: "",
});
} finally {
await killChild(holder);
disposeChildOutput(holder);
rmSync(workDir, { recursive: true, force: true });
}
});
});