Tests first: crashed-lock race, in-flight fence, SIGTERM stop, foreign reclaim, port range

Red at 4fd3f60 for stale-lock-takeover-race, fence-admits-in-flight-request and
bind-port-65536-accepted; green guards for reclaim-root-unpinned and the SIGTERM
property. The daemon's stop() no longer leaves a daemon holding the run open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-24 03:01:17 +02:00
commit 17e84c65da

View file

@ -4,6 +4,7 @@ import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import http from "node:http";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import test from "node:test";
@ -91,11 +92,16 @@ async function startDaemon({
spawns: () => spawnLog().filter((line) => /^\d+$/.test(line)).map(Number),
grandchildren: () => spawnLog().filter((line) => line.startsWith("grandchild ")).map((line) => Number(line.split(" ")[1])),
async stop() {
if (proc.exitCode === null && proc.signalCode === null) {
proc.kill("SIGTERM");
await until(() => proc.exitCode !== null || proc.signalCode !== null, "the daemon to exit", 30);
try {
if (proc.exitCode === null && proc.signalCode === null) {
proc.kill("SIGTERM");
await until(() => proc.exitCode !== null || proc.signalCode !== null, "the daemon to exit", 30);
}
} finally {
// a daemon that did not stop on SIGTERM must not outlive the test and hold the run open
if (proc.exitCode === null && proc.signalCode === null) proc.kill("SIGKILL");
rmSync(root, { recursive: true, force: true });
}
rmSync(root, { recursive: true, force: true });
},
};
}
@ -449,6 +455,27 @@ test("a live worker outside the pid file's group is left alone", async () => {
}
});
// The marker that proves a group is ours is this state root's own embeddings directory. A live worker of another
// state root, named by a stale pid file, carries another root's marker and is left alone.
test("a pid file naming another state root's live worker kills nothing", async () => {
const other = await startDaemon();
const root = mkdtempSync(path.join(os.tmpdir(), "vili-test-"));
try {
assert.equal((await generate(other)).status, 200);
const [foreign] = other.spawns();
writeFileSync(path.join(root, "worker.pid"), `${foreign}\n`);
const daemon = await startDaemon({ root });
try {
assert.ok(isAlive(foreign), "a worker carrying another root's marker must survive the reclaim");
assert.equal(existsSync(path.join(root, "worker.pid")), false, "the stale pid file is still forgotten");
} finally {
await daemon.stop();
}
} finally {
await other.stop();
}
});
test("a pid file naming a process group that is not the worker's kills nothing", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "vili-test-"));
const bystander = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { detached: true, stdio: "ignore" });
@ -711,6 +738,34 @@ test("a second daemon on a live state root exits 2 and kills nothing", async ()
}
});
// Two daemons start together over the lock a crashed daemon left. Exactly one owns the root, every time: the
// other exits 2, and one generate leaves one worker on the root.
for (let trial = 1; trial <= 40; trial += 1) {
test(`two daemons racing a crashed daemon's lock leave one owner and one worker (trial ${trial})`, async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "vili-test-"));
const crashed = await startDaemon({ root });
crashed.proc.kill("SIGKILL");
await until(() => crashed.proc.exitCode !== null || crashed.proc.signalCode !== null, "the first daemon to die");
const racers = await Promise.all([startDaemon({ root, hold: false }), startDaemon({ root, hold: false })]);
try {
await until(() => racers.every((racer) => racer.proc.exitCode !== null || /holds the state root/.test(racer.stdout())), "both racers to decide", 20);
await new Promise((resolve) => setTimeout(resolve, 300));
const owners = racers.filter((racer) => racer.proc.exitCode === null);
assert.equal(owners.length, 1, "exactly one daemon stays up");
assert.match(owners[0].stdout(), /holds the state root/);
const loser = racers.find((racer) => racer !== owners[0]);
assert.equal(loser.proc.exitCode, 2);
assert.match(loser.stderr(), /held by a running daemon/);
for (const racer of owners) assert.equal((await generate(racer)).status, 200);
assert.equal(aliveWorkers(owners[0]).length, 1, "one worker on the root");
} finally {
for (const racer of racers) if (racer.proc.exitCode === null && racer.proc.signalCode === null) racer.proc.kill("SIGKILL");
for (const pid of racers[0].spawns()) { try { process.kill(-pid, "SIGKILL"); } catch { /* gone */ } }
rmSync(root, { recursive: true, force: true });
}
});
}
const idunnDaemon = async ({ leased = false, extraEnv = {} } = {}) => {
const root = mkdtempSync(path.join(os.tmpdir(), "vili-test-"));
const lease = path.join(root, "lease");
@ -776,15 +831,107 @@ test("losing the lease kills the worker tree, refuses, and writes nothing more",
}
});
// A generate whose headers are in and whose body is still arriving when the lease goes. The lease is gone, so
// nothing is created and nothing is spawned when the body completes, whether the daemon has noticed the loss yet
// (its 5 s tick) or not.
for (const noticed of [false, true]) {
test(`a request in flight when the lease is lost writes and spawns nothing (loss ${noticed ? "noticed" : "not yet noticed"})`, async () => {
const daemon = await idunnDaemon({ leased: true });
try {
await until(() => existsSync(path.join(daemon.stateRoot, "vili.service.cc")), "the service store", 10);
const body = JSON.stringify({ prompt: "x", duration: 2 });
const socket = net.connect(daemon.port, "127.0.0.1");
let raw = "";
socket.on("data", (chunk) => { raw += chunk; });
const closed = new Promise((resolve) => socket.on("close", resolve));
socket.write(`POST /motion/generate HTTP/1.1\r\nhost: vili\r\ncontent-type: application/json\r\ncontent-length: ${body.length}\r\nconnection: close\r\n\r\n${body.slice(0, 5)}`);
await new Promise((resolve) => setTimeout(resolve, 200));
rmSync(daemon.lease);
if (noticed) await until(() => daemon.health().join() === "active,degraded", "the daemon to notice the loss", 15);
socket.write(body.slice(5));
await Promise.race([closed, new Promise((resolve) => setTimeout(resolve, 15000))]);
socket.destroy();
assert.match(raw, /^HTTP\/1\.1 503 /);
assert.match(raw, /write-lease-lost/);
assert.deepEqual(daemon.spawns(), [], "no worker is spawned after the loss");
const artifacts = path.join(daemon.stateRoot, "artifacts");
assert.deepEqual(existsSync(artifacts) ? readdirSync(artifacts) : [], [], "no artifact directory is created");
assert.deepEqual(readdirSync(path.join(daemon.stateRoot, "jobs")), []);
} finally {
await daemon.stop();
}
});
}
test("a request arriving right after the lease is lost writes and spawns nothing", async () => {
const daemon = await idunnDaemon({ leased: true });
try {
await until(() => existsSync(path.join(daemon.stateRoot, "vili.service.cc")), "the service store", 10);
rmSync(daemon.lease);
const answer = await generate(daemon);
assert.equal(answer.status, 503);
assert.equal(answer.body.code, "write-lease-lost");
assert.deepEqual(daemon.spawns(), []);
const artifacts = path.join(daemon.stateRoot, "artifacts");
assert.deepEqual(existsSync(artifacts) ? readdirSync(artifacts) : [], []);
assert.deepEqual(readdirSync(path.join(daemon.stateRoot, "jobs")), []);
} finally {
await daemon.stop();
}
});
test("a worker respawned under the lease is killed when the lease is lost", async () => {
const daemon = await idunnDaemon({ leased: true });
try {
await until(() => existsSync(path.join(daemon.stateRoot, "vili.service.cc")), "the service store", 10);
assert.equal((await generate(daemon)).status, 200);
const [first] = daemon.spawns();
process.kill(-first, "SIGKILL");
await until(async () => (await backendOf(daemon)).state === "stopped", "the daemon to see the worker gone");
assert.equal((await generate(daemon)).status, 200);
const second = daemon.spawns().find((pid) => pid !== first);
assert.ok(second && isAlive(second));
rmSync(daemon.lease);
await until(() => !isAlive(second), "the respawned worker to die", 20);
} finally {
await daemon.stop();
}
});
// Idunn stops an incumbent (SIGTERM to the unit) before it revokes the lease. What Vili owns in that sequence: the
// worker tree is dead when the process ends, it exits 0, and nothing is written from the signal on.
test("on SIGTERM the daemon stops its worker tree, writes nothing more, and exits 0", async () => {
const daemon = await idunnDaemon({ leased: true });
try {
await until(() => existsSync(path.join(daemon.stateRoot, "vili.service.cc")), "the service store", 10);
assert.equal((await generate(daemon, { prompt: "x", model: "stubborn" })).status, 200);
const [worker] = daemon.spawns();
const [grandchild] = await until(() => { const found = daemon.grandchildren(); return found.length ? found : null; }, "the armed grandchild");
const store = path.join(daemon.stateRoot, "vili.service.cc");
const before = statSync(store).mtimeMs;
const jobs = readdirSync(path.join(daemon.stateRoot, "jobs"));
daemon.proc.kill("SIGTERM");
await until(() => daemon.proc.exitCode !== null || daemon.proc.signalCode !== null, "the daemon to exit", 30);
assert.equal(daemon.proc.exitCode, 0);
assert.ok(!isAlive(worker) && !isAlive(grandchild), "the worker tree is dead when the daemon is gone");
assert.equal(statSync(store).mtimeMs, before, "the 5 s surface tick must not write during the stop");
assert.deepEqual(readdirSync(path.join(daemon.stateRoot, "jobs")), jobs);
assert.ok(existsSync(daemon.lease), "the daemon never touches the lease Idunn revokes");
} finally {
await daemon.stop();
}
});
test("a job that finishes after the lease is lost is not recorded", async () => {
const daemon = await idunnDaemon({ leased: true });
try {
await until(() => existsSync(path.join(daemon.stateRoot, "vili.service.cc")), "the service store", 10);
const slow = generate(daemon, { prompt: "x", model: "slow-generate" });
slow.catch(() => {}); // a failing assertion below must not leave this request pending
await until(() => daemon.spawns().length, "the worker");
await until(() => request(daemon.port, "GET", "/health").then((r) => r.body?.state?.backend?.state === "ready"), "the worker to be ready");
rmSync(daemon.lease);
const answer = await slow;
const answer = await Promise.race([slow, new Promise((resolve) => setTimeout(() => resolve({ status: "no answer in 30 s" }), 30000))]);
assert.equal(answer.status, 503);
assert.equal(answer.body.code, "write-lease-lost");
assert.deepEqual(readdirSync(path.join(daemon.stateRoot, "jobs")), []);
@ -864,10 +1011,13 @@ test("the entry takes exactly --state-root <absolute> --kimodo-commit <40 hex>,
assert.equal(answer.code, 2, JSON.stringify(args));
assert.ok(!answer.output.includes("canary"), `the refusal repeated a value of ${JSON.stringify(args)}`);
}
const bind = await runEntry(good, { GAMECULT_IDUNN_CANDIDATE_BIND: "canary-not-a-bind" });
assert.equal(bind.code, 2);
assert.match(bind.output, /CANDIDATE_BIND is not host:port/);
assert.ok(!bind.output.includes("canary"));
for (const badBind of ["canary-not-a-bind", "127.0.0.1:65536", "127.0.0.1:99999", "127.0.0.1:123456", "127.0.0.1:"]) {
const bind = await runEntry(good, { GAMECULT_IDUNN_CANDIDATE_BIND: badBind });
assert.equal(bind.code, 2, badBind);
assert.match(bind.output, /CANDIDATE_BIND is not host:port/);
assert.ok(!bind.output.includes("canary"));
assert.ok(!bind.output.includes("65536") && !bind.output.includes("99999"));
}
});
test("the entry derives the daemon's host, port, interpreter and environment identity from the release", async () => {
@ -880,6 +1030,7 @@ test("the entry derives the daemon's host, port, interpreter and environment ide
writeFileSync(path.join(release, "vili", "deployment", "idunn", "requirements-build.lock"), "build lock bytes\n");
writeFileSync(path.join(release, "vili", "deployment", "idunn", "requirements.lock"), "runtime lock bytes\n");
const entryDir = path.join(release, "vili", "scripts");
assert.equal(daemonEnvironment("127.0.0.1:65535", entryDir).VILI_DAEMON_PORT, "65535", "the top of the port range is accepted");
for (const bind of ["127.0.0.1:18861", "http://127.0.0.1:18861"]) {
const derived = daemonEnvironment(bind, entryDir);
assert.equal(derived.VILI_DAEMON_HOST, "127.0.0.1");