88 lines
3.5 KiB
JavaScript
88 lines
3.5 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { Worker } = require("worker_threads");
|
|
const {
|
|
writeJsonAtomicSync,
|
|
clearSafeFileDiagnostics,
|
|
getSafeFileDiagnostics
|
|
} = require("../src/services/safe-files");
|
|
|
|
async function run() {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-safe-files-"));
|
|
const target = path.join(directory, "runtime_state.json");
|
|
const originalRename = fs.renameSync;
|
|
try {
|
|
fs.writeFileSync(target, '{"state":"original"}\n');
|
|
const stale = `${target}.tmp`;
|
|
fs.writeFileSync(stale, "stale");
|
|
const old = new Date(Date.now() - 2 * 60 * 60 * 1000);
|
|
fs.utimesSync(stale, old, old);
|
|
|
|
clearSafeFileDiagnostics();
|
|
let transientFailures = 0;
|
|
fs.renameSync = (from, to) => {
|
|
if (to === target && String(from).endsWith(".tmp") && transientFailures < 2) {
|
|
transientFailures += 1;
|
|
throw Object.assign(new Error("temporarily locked"), { code: transientFailures === 1 ? "EPERM" : "EACCES" });
|
|
}
|
|
return originalRename(from, to);
|
|
};
|
|
writeJsonAtomicSync(target, { state: "recovered" }, { attempts: 4, delayMs: 1, staleTempAgeMs: 1000 });
|
|
assert.deepEqual(JSON.parse(fs.readFileSync(target, "utf8")), { state: "recovered" });
|
|
assert.equal(fs.existsSync(stale), false);
|
|
assert(getSafeFileDiagnostics().filter((entry) => entry.operation === "rename").length >= 2);
|
|
|
|
fs.writeFileSync(target, '{"state":"must-survive"}\n');
|
|
fs.renameSync = (from, to) => {
|
|
if (to === target && String(from).endsWith(".tmp")) {
|
|
throw Object.assign(new Error("replacement locked"), { code: "EPERM" });
|
|
}
|
|
return originalRename(from, to);
|
|
};
|
|
assert.throws(
|
|
() => writeJsonAtomicSync(target, { state: "must-not-land" }, { attempts: 2, delayMs: 1 }),
|
|
/replacement locked/
|
|
);
|
|
assert.deepEqual(JSON.parse(fs.readFileSync(target, "utf8")), { state: "must-survive" });
|
|
|
|
fs.renameSync = originalRename;
|
|
await Promise.all(Array.from({ length: 4 }, (_, worker) => runWorker(target, worker)));
|
|
const final = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
assert(Number.isInteger(final.worker));
|
|
assert(Number.isInteger(final.write));
|
|
assert.equal(fs.readdirSync(directory).some((name) => name.endsWith(".tmp")), false);
|
|
} finally {
|
|
fs.renameSync = originalRename;
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
console.log("Safe atomic file verification passed: transient lock retry, rollback, stale cleanup, and concurrent writes.");
|
|
}
|
|
|
|
function runWorker(target, worker) {
|
|
const helper = require.resolve("../src/services/safe-files");
|
|
const source = `
|
|
const { parentPort, workerData } = require("worker_threads");
|
|
const { writeJsonAtomicSync } = require(workerData.helper);
|
|
try {
|
|
for (let write = 0; write < 12; write += 1) {
|
|
writeJsonAtomicSync(workerData.target, { worker: workerData.worker, write });
|
|
}
|
|
parentPort.postMessage({ ok: true });
|
|
} catch (error) {
|
|
parentPort.postMessage({ ok: false, error: error.stack || error.message });
|
|
}
|
|
`;
|
|
return new Promise((resolve, reject) => {
|
|
const instance = new Worker(source, { eval: true, workerData: { helper, target, worker } });
|
|
instance.once("message", (message) => message.ok ? resolve() : reject(new Error(message.error)));
|
|
instance.once("error", reject);
|
|
});
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|