fix: harden embedded Pi lifecycle recovery

This commit is contained in:
2026-08-04 23:14:47 +02:00
parent a368889838
commit 935bb1db0e
15 changed files with 572 additions and 101 deletions
+32 -5
View File
@@ -1,5 +1,5 @@
import { test, expect } from "vitest";
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { MaintenanceBarrier } from "../src/runtime/maintenance-gate.js";
@@ -55,7 +55,7 @@ test("activation reconciles active state when directory fsync fails after marker
});
await expect(gate.activate()).rejects.toThrow(/post-rename fsync failure/);
expect(existsSync(marker)).toBe(true);
expect(gate.status()).toEqual({ active: true, admissions: 0 });
expect(gate.status()).toEqual({ active: true, admissions: 0, recoveryRequired: true });
expect(gate.acquire()).toBeUndefined();
const recovered = new MaintenanceBarrier(marker);
@@ -66,6 +66,33 @@ test("activation reconciles active state when directory fsync fails after marker
}
});
test.each(["write", "fsync"] as const)(
"activation cleans its marker temp file after a pre-rename %s failure",
async (failure) => {
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-temp-cleanup-"));
const marker = join(directory, "maintenance.json");
try {
const gate = new MaintenanceBarrier(marker, {
writeFile(descriptor, contents) {
if (failure === "write") throw new Error("injected marker write failure");
writeFileSync(descriptor, contents, "utf8");
},
syncFile() {
if (failure === "fsync") throw new Error("injected marker fsync failure");
},
syncDirectory() {},
});
await expect(gate.activate()).rejects.toThrow(`injected marker ${failure} failure`);
expect(existsSync(marker)).toBe(false);
expect(readdirSync(directory).filter((entry) => entry.includes(".tmp-"))).toEqual([]);
expect(gate.status()).toEqual({ active: false, admissions: 0, recoveryRequired: true });
} finally {
rmSync(directory, { recursive: true, force: true });
}
},
);
test("deactivation reconciles inactive state when directory fsync fails after marker removal", async () => {
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-deactivate-fsync-"));
const marker = join(directory, "maintenance.json");
@@ -79,9 +106,9 @@ test("deactivation reconciles inactive state when directory fsync fails after ma
await gate.activate();
failSync = true;
expect(() => gate.deactivate()).toThrow(/post-remove fsync failure/);
expect(existsSync(marker)).toBe(false);
expect(gate.status()).toEqual({ active: false, admissions: 0 });
expect(gate.acquire()).toBeTypeOf("function");
expect(existsSync(marker)).toBe(true);
expect(gate.status()).toEqual({ active: true, admissions: 0, recoveryRequired: true });
expect(gate.acquire()).toBeUndefined();
} finally {
rmSync(directory, { recursive: true, force: true });
}
+13 -3
View File
@@ -170,15 +170,25 @@ test("maintenance endpoints report marker-derived state after post-rename and po
const activated = await app.inject({ method: "POST", url: "/internal/maintenance/activate" });
expect(activated.statusCode).toBe(500);
expect(activated.json()).toMatchObject({ active: true, admissions: 0 });
expect(activated.json()).toMatchObject({
active: true,
admissions: 0,
recoveryRequired: true,
code: "maintenance_durability_failed",
});
failSync = false;
expect((await app.inject({ method: "GET", url: "/internal/maintenance/status" })).json())
.toEqual({ active: true, admissions: 0 });
.toEqual({ active: true, admissions: 0, recoveryRequired: true });
failSync = true;
const deactivated = await app.inject({ method: "POST", url: "/internal/maintenance/deactivate" });
expect(deactivated.statusCode).toBe(500);
expect(deactivated.json()).toMatchObject({ active: false, admissions: 0 });
expect(deactivated.json()).toMatchObject({
active: true,
admissions: 0,
recoveryRequired: true,
code: "maintenance_durability_failed",
});
} finally {
rmSync(dir, { recursive: true, force: true });
}