fix: address Copilot review round 2 (#2339)
- The holder-record removal is best-effort and can no longer throw past release(); ERELEASED on release is the library having already dropped a taken-over lock, so it is ignored rather than described as a lock that "goes stale on its own". - The signal test's stand-in now spawns a descendant of its own, so the whole-process-group guarantee is what the test asserts: a regression to child.kill() or a non-detached spawn leaves the descendant alive and fails. - The three docs no longer claim a dead holder is always recovered in 30s: a stale lock directory that cannot be removed runs the wait to its 45-minute cap and names the path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
60bbe7675f
commit
2c8dbd3cd0
2 changed files with 35 additions and 12 deletions
|
|
@ -444,12 +444,21 @@ export async function runUnderLease({
|
|||
// waiter reading a fresh lock must not be told about the previous
|
||||
// holder — unless the lock is no longer ours, in which case the record
|
||||
// is the winner's too.
|
||||
if (guarded.mine()) rmSync(leaseTarget(dir), { force: true });
|
||||
try {
|
||||
if (guarded.mine()) rmSync(leaseTarget(dir), { force: true });
|
||||
} catch {
|
||||
// Diagnostic only; never let it stand between a finished gate and the
|
||||
// release below.
|
||||
}
|
||||
try {
|
||||
await release();
|
||||
} catch (err) {
|
||||
// `ERELEASED` means the library's own refresh tick already found the
|
||||
// lock taken over and dropped it; the directory there now is the
|
||||
// winner's live lock, and `onCompromised` has already said so.
|
||||
if (err?.code === "ERELEASED") return;
|
||||
log(
|
||||
`gate-lease: could not release the lease (${err?.message ?? err}); it goes stale on its own after ${formatDuration(STALE_MS)}.`,
|
||||
`gate-lease: could not release the lease (${err?.message ?? err}). A waiter takes it over once it is ${formatDuration(STALE_MS)} stale — unless whatever blocked this removal persists, in which case remove ${lockPathOf(dir)} by hand.`,
|
||||
);
|
||||
}
|
||||
log(
|
||||
|
|
|
|||
|
|
@ -379,9 +379,14 @@ test("main: no command is a usage error", async () => {
|
|||
test("a signal to the wrapper stops the whole tree, releases the lease and exits 128+signal", async () => {
|
||||
const dir = freshDir();
|
||||
const pidFile = join(dir, "grandchild.pid");
|
||||
// The gate stand-in: records its pid, then idles forever unless signalled.
|
||||
// The gate stand-in: starts a descendant of its own (as `npm run` does,
|
||||
// four levels deep), records both pids, then idles forever unless
|
||||
// signalled. The descendant is what proves the whole *tree* is stopped —
|
||||
// a regression to `child.kill()` or a non-detached spawn would leave it
|
||||
// running and fail below.
|
||||
const idle =
|
||||
`require("fs").writeFileSync(${JSON.stringify(pidFile)}, String(process.pid));` +
|
||||
`const d = require("child_process").spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" });` +
|
||||
`require("fs").writeFileSync(${JSON.stringify(pidFile)}, process.pid + " " + d.pid);` +
|
||||
`setInterval(() => {}, 1000)`;
|
||||
const wrapper = spawn(
|
||||
process.execPath,
|
||||
|
|
@ -397,11 +402,12 @@ test("a signal to the wrapper stops the whole tree, releases the lease and exits
|
|||
wrapper.once("exit", (code, signal) => resolve({ code, signal })),
|
||||
);
|
||||
|
||||
let grandchild = null;
|
||||
let tree = [];
|
||||
try {
|
||||
await waitFor(() => existsSync(pidFile));
|
||||
grandchild = Number(readFileSync(pidFile, "utf8"));
|
||||
assert.ok(isAlive(grandchild));
|
||||
tree = readFileSync(pidFile, "utf8").split(" ").map(Number);
|
||||
assert.equal(tree.length, 2);
|
||||
for (const pid of tree) assert.ok(isAlive(pid), `pid ${pid} started`);
|
||||
assert.ok(existsSync(lockPathOf(dir)), "the wrapper held the lease");
|
||||
} finally {
|
||||
// On every path — a failed setup assertion included — the wrapper is
|
||||
|
|
@ -410,7 +416,11 @@ test("a signal to the wrapper stops the whole tree, releases the lease and exits
|
|||
}
|
||||
const outcome = await exited;
|
||||
assert.deepEqual(outcome, { code: 143, signal: null }, stderr);
|
||||
await waitFor(() => !isAlive(grandchild));
|
||||
for (const pid of tree)
|
||||
await waitFor(() => !isAlive(pid)).catch(() => {
|
||||
process.kill(pid, "SIGKILL");
|
||||
assert.fail(`pid ${pid} outlived the wrapper — the tree was not stopped`);
|
||||
});
|
||||
assert.ok(
|
||||
!existsSync(lockPathOf(dir)),
|
||||
"the lease was released on the way out",
|
||||
|
|
|
|||
Loading…
Reference in a new issue