mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
286 lines
9.7 KiB
JavaScript
286 lines
9.7 KiB
JavaScript
/* eslint-disable node/no-sync */
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs/promises";
|
|
import fsSync from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import { LockFile } from "@clusterio/lib";
|
|
|
|
const baseDir = path.join("temp", "test", "lock_file");
|
|
const ownContent = `${process.pid} ${performance.timeOrigin}\n`;
|
|
// The parent process (mocha) is alive but isn't this process
|
|
const otherContent = `${process.ppid}\n`;
|
|
const exitedContent = "999999999\n";
|
|
|
|
describe("class LockFile", function () {
|
|
/** @type {string} */
|
|
let filePath;
|
|
/** @type {string} */
|
|
let recoveryPath;
|
|
/** @type {LockFile} */
|
|
let lock;
|
|
const writeFile = fs.writeFile;
|
|
|
|
before(async function () {
|
|
await fs.mkdir(baseDir, { recursive: true });
|
|
});
|
|
|
|
beforeEach(function() {
|
|
filePath = path.join(baseDir, `${Date.now()}-${Math.random()}.lock`);
|
|
recoveryPath = `${filePath}.recovery`;
|
|
lock = new LockFile(filePath);
|
|
});
|
|
|
|
afterEach(async function () {
|
|
fs.writeFile = writeFile;
|
|
if (await lock.isHeld()) {
|
|
await lock.release();
|
|
}
|
|
await fs.unlink(filePath).catch(() => {});
|
|
await fs.unlink(recoveryPath).catch(() => {});
|
|
});
|
|
|
|
async function writeOld(content) {
|
|
await fs.writeFile(filePath, content);
|
|
const old = new Date(Date.now() - 60e3);
|
|
await fs.utimes(filePath, old, old);
|
|
return old;
|
|
}
|
|
|
|
describe("acquire()", function () {
|
|
it("should create the lock file when acquiring", async function () {
|
|
await lock.acquire();
|
|
assert.equal(await fs.readFile(filePath, "utf8"), ownContent);
|
|
});
|
|
|
|
it("should throw LockFileBusyError if called when not idle", async function () {
|
|
await lock.acquire();
|
|
await assert.rejects(lock.acquire(), { name: "LockFileBusyError" });
|
|
});
|
|
|
|
it("should remove stale lock file and acquire it", async function () {
|
|
await fs.writeFile(filePath, exitedContent);
|
|
await lock.acquire();
|
|
assert.equal(await fs.readFile(filePath, "utf8"), ownContent);
|
|
await assert.rejects(fs.access(recoveryPath));
|
|
});
|
|
|
|
it("should throw if existing lock file is not stale", async function () {
|
|
await fs.writeFile(filePath, otherContent);
|
|
await assert.rejects(lock.acquire(), { name: "LockFileExistsError" });
|
|
});
|
|
|
|
it("should throw if another lock in this process holds the file", async function () {
|
|
const other = new LockFile(filePath);
|
|
await other.acquire();
|
|
await assert.rejects(lock.acquire(), { name: "LockFileExistsError" });
|
|
await other.release();
|
|
});
|
|
|
|
it("should acquire a lock left by an earlier process with the same pid", async function () {
|
|
for (const content of [`${process.pid}\n`, `${process.pid} 1234.5\n`]) {
|
|
await fs.writeFile(filePath, content);
|
|
await lock.acquire();
|
|
await lock.release();
|
|
}
|
|
});
|
|
|
|
it("should throw while the lock file may still be being written", async function () {
|
|
await fs.writeFile(filePath, "");
|
|
await assert.rejects(lock.acquire(), { name: "LockFileExistsError" });
|
|
});
|
|
|
|
it("should acquire an unreadable lock that is old", async function () {
|
|
await writeOld("");
|
|
await lock.acquire();
|
|
});
|
|
|
|
// Runs takeOver right before this process starts clearing the stale lock, as another process could
|
|
function beforeRecovery(takeOver) {
|
|
fs.writeFile = async function(...args) {
|
|
if (String(args[0]).endsWith(".recovery")) {
|
|
fs.writeFile = writeFile;
|
|
await takeOver();
|
|
}
|
|
return writeFile(...args);
|
|
};
|
|
}
|
|
|
|
it("should leave a lock taken after the stale check in place", async function () {
|
|
await fs.writeFile(filePath, exitedContent);
|
|
beforeRecovery(async () => {
|
|
await fs.rm(filePath);
|
|
await fs.writeFile(filePath, otherContent);
|
|
});
|
|
await assert.rejects(lock.acquire(), { name: "LockFileExistsError" });
|
|
assert.equal(await fs.readFile(filePath, "utf8"), otherContent);
|
|
await assert.rejects(fs.access(recoveryPath));
|
|
});
|
|
|
|
it("should leave a new empty lock in place of an old empty lock", async function () {
|
|
const old = await writeOld("");
|
|
// Another process clears it and creates a new lock that it hasn't written its pid to yet
|
|
beforeRecovery(async () => {
|
|
await fs.rm(filePath);
|
|
await fs.writeFile(filePath, "");
|
|
});
|
|
await assert.rejects(lock.acquire(), { name: "LockFileExistsError" });
|
|
assert.equal(await fs.readFile(filePath, "utf8"), "");
|
|
assert((await fs.stat(filePath)).mtimeMs > old.getTime() + 30e3);
|
|
});
|
|
|
|
it("should leave a stale lock alone while another process clears it", async function () {
|
|
await fs.writeFile(filePath, exitedContent);
|
|
await fs.writeFile(recoveryPath, otherContent);
|
|
await assert.rejects(lock.acquire(), { name: "LockFileExistsError" });
|
|
assert.equal(await fs.readFile(filePath, "utf8"), exitedContent);
|
|
assert.equal(await fs.readFile(recoveryPath, "utf8"), otherContent);
|
|
});
|
|
|
|
it("should throw LockFileRecoveryError if clearing a stale lock was interrupted", async function () {
|
|
await fs.writeFile(filePath, exitedContent);
|
|
await fs.writeFile(recoveryPath, exitedContent);
|
|
await assert.rejects(lock.acquire(), { name: "LockFileRecoveryError" });
|
|
});
|
|
});
|
|
|
|
describe("release()", function () {
|
|
it("should remove the lock file when releasing", async function () {
|
|
await lock.acquire();
|
|
await lock.release();
|
|
await assert.rejects(fs.access(filePath));
|
|
});
|
|
|
|
it("should throw LockFileBusyError if called when not acquired", async function () {
|
|
await assert.rejects(lock.release(), { name: "LockFileBusyError" });
|
|
});
|
|
|
|
it("should leave a lock file it does not own in place", async function () {
|
|
await lock.acquire();
|
|
await fs.writeFile(filePath, otherContent);
|
|
await lock.release();
|
|
assert.equal(await fs.readFile(filePath, "utf8"), otherContent);
|
|
});
|
|
});
|
|
|
|
describe("isHeld()", function () {
|
|
it("should return true only when lock file contains current PID", async function () {
|
|
await lock.acquire();
|
|
const held = await lock.isHeld();
|
|
assert.equal(held, true);
|
|
});
|
|
|
|
it("should return false when lock file does not exist", async function () {
|
|
const held = await lock.isHeld();
|
|
assert.equal(held, false);
|
|
});
|
|
|
|
it("should return false if lock file contains different PID", async function () {
|
|
await fs.writeFile(filePath, otherContent);
|
|
const held = await lock.isHeld();
|
|
assert.equal(held, false);
|
|
});
|
|
|
|
it("should return false if lock file was deleted while held", async function() {
|
|
await lock.acquire();
|
|
await fs.unlink(filePath);
|
|
const held = await lock.isHeld();
|
|
assert.equal(held, false);
|
|
});
|
|
});
|
|
|
|
describe("isStale()", function () {
|
|
it("should return true if lock file does not exist", async function () {
|
|
const stale = await lock.isStale();
|
|
assert.equal(stale, true);
|
|
});
|
|
|
|
it("should return true for invalid PID in an old lock file", async function () {
|
|
await writeOld("abc\n");
|
|
const stale = await lock.isStale();
|
|
assert.equal(stale, true);
|
|
});
|
|
|
|
it("should return false for invalid PID in a new lock file", async function () {
|
|
await fs.writeFile(filePath, "abc\n");
|
|
const stale = await lock.isStale();
|
|
assert.equal(stale, false);
|
|
});
|
|
|
|
it("should return true for non-existent process PID", async function () {
|
|
await fs.writeFile(filePath, exitedContent);
|
|
const stale = await lock.isStale();
|
|
assert.equal(stale, true);
|
|
});
|
|
|
|
it("should return false for another running process", async function () {
|
|
await fs.writeFile(filePath, otherContent);
|
|
const stale = await lock.isStale();
|
|
assert.equal(stale, false);
|
|
});
|
|
|
|
it("should return false for a lock taken by this process", async function () {
|
|
await lock.acquire();
|
|
const stale = await new LockFile(filePath).isStale();
|
|
assert.equal(stale, false);
|
|
});
|
|
|
|
it("should return true for an earlier process with the same pid", async function () {
|
|
await fs.writeFile(filePath, `${process.pid}\n`);
|
|
const stale = await lock.isStale();
|
|
assert.equal(stale, true);
|
|
});
|
|
});
|
|
|
|
describe("holderPid()", function () {
|
|
it("should return the pid in the lock file", async function () {
|
|
await fs.writeFile(filePath, `${process.ppid} 1234.5\n`);
|
|
assert.equal(await lock.holderPid(), process.ppid);
|
|
});
|
|
|
|
it("should return undefined without a valid pid", async function () {
|
|
assert.equal(await lock.holderPid(), undefined);
|
|
await fs.writeFile(filePath, "");
|
|
assert.equal(await lock.holderPid(), undefined);
|
|
});
|
|
});
|
|
|
|
describe("attach/detach exit handlers", function () {
|
|
it("should attach exit handler on acquire and detach on release", async function () {
|
|
await lock.acquire();
|
|
const listenersBefore = process.listeners("exit").slice();
|
|
assert.ok(listenersBefore.includes(lock.handleExitSync));
|
|
assert.ok(process.listeners("SIGTERM").includes(lock.handleExit));
|
|
|
|
await lock.release();
|
|
const listenersAfter = process.listeners("exit").slice();
|
|
assert.ok(!listenersAfter.includes(lock.handleExitSync));
|
|
assert.ok(!process.listeners("SIGTERM").includes(lock.handleExit));
|
|
});
|
|
|
|
it("should only attach the exit handler without handleSignals", async function () {
|
|
lock = new LockFile(filePath, false);
|
|
await lock.acquire();
|
|
assert.ok(process.listeners("exit").includes(lock.handleExitSync));
|
|
for (const event of ["SIGINT", "SIGTERM", "SIGHUP", "uncaughtException"]) {
|
|
assert.ok(!process.listeners(event).includes(lock.handleExit), event);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("handleExitSync()", function () {
|
|
it("should not throw if file already removed", function () {
|
|
assert.doesNotThrow(() => lock.handleExitSync()); // simulate non-existent file
|
|
});
|
|
|
|
it("should only remove a lock file it owns", async function () {
|
|
await fs.writeFile(filePath, otherContent);
|
|
lock.handleExitSync();
|
|
assert.equal(fsSync.readFileSync(filePath, "utf8"), otherContent);
|
|
await fs.writeFile(filePath, ownContent);
|
|
lock.handleExitSync();
|
|
assert.equal(fsSync.existsSync(filePath), false);
|
|
});
|
|
});
|
|
});
|