103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
import { test, expect } from "vitest";
|
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { MaintenanceBarrier } from "../src/runtime/maintenance-gate.js";
|
|
|
|
test("activation waits for an in-flight admission lease and rejects later admissions", async () => {
|
|
const gate = new MaintenanceBarrier();
|
|
const release = gate.acquire();
|
|
expect(release).toBeTypeOf("function");
|
|
let acknowledged = false;
|
|
const activation = gate.activate().then(() => { acknowledged = true; });
|
|
await Promise.resolve();
|
|
expect(acknowledged).toBe(false);
|
|
expect(gate.acquire()).toBeUndefined();
|
|
release?.();
|
|
await activation;
|
|
expect(acknowledged).toBe(true);
|
|
expect(gate.status()).toEqual({ active: true, admissions: 0 });
|
|
gate.deactivate();
|
|
expect(gate.acquire()).toBeTypeOf("function");
|
|
});
|
|
|
|
test("durable activation survives recreation and deactivation removes the marker first", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-gate-"));
|
|
const marker = join(directory, "maintenance.json");
|
|
try {
|
|
const first = new MaintenanceBarrier(marker);
|
|
const release = first.acquire();
|
|
expect(release).toBeTypeOf("function");
|
|
const activation = first.activate();
|
|
expect(existsSync(marker)).toBe(true);
|
|
expect(first.acquire()).toBeUndefined();
|
|
expect(first.status()).toEqual({ active: true, admissions: 1 });
|
|
release?.();
|
|
await activation;
|
|
expect(first.status()).toEqual({ active: true, admissions: 0 });
|
|
|
|
const recreated = new MaintenanceBarrier(marker);
|
|
expect(recreated.status()).toEqual({ active: true, admissions: 0 });
|
|
recreated.deactivate();
|
|
expect(existsSync(marker)).toBe(false);
|
|
expect(recreated.status()).toEqual({ active: false, admissions: 0 });
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("activation reconciles active state when directory fsync fails after marker rename", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-activate-fsync-"));
|
|
const marker = join(directory, "maintenance.json");
|
|
try {
|
|
const gate = new MaintenanceBarrier(marker, {
|
|
syncDirectory() { throw new Error("injected post-rename fsync failure"); },
|
|
});
|
|
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.acquire()).toBeUndefined();
|
|
|
|
const recovered = new MaintenanceBarrier(marker);
|
|
expect(recovered.status()).toEqual({ active: true, admissions: 0 });
|
|
expect(recovered.acquire()).toBeUndefined();
|
|
} 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");
|
|
let failSync = false;
|
|
try {
|
|
const gate = new MaintenanceBarrier(marker, {
|
|
syncDirectory() {
|
|
if (failSync) throw new Error("injected post-remove fsync failure");
|
|
},
|
|
});
|
|
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");
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("status and admission recover from a persistent marker even when memory started inactive", () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-reconcile-"));
|
|
const marker = join(directory, "maintenance.json");
|
|
try {
|
|
const gate = new MaintenanceBarrier(marker);
|
|
expect(gate.status().active).toBe(false);
|
|
writeFileSync(marker, '{"version":1,"active":true}\n', { mode: 0o600 });
|
|
expect(gate.status()).toEqual({ active: true, admissions: 0 });
|
|
expect(gate.acquire()).toBeUndefined();
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|