pi-mono/packages/chord/test/next-node/workers/lock.test.ts
Mario Zechner 8959ad6e68 feat(chord): Chord v2 slice 10, HTTP and workers
chord/next/http: a Web fetch handler built from the registry Http (routes, wraps,
sockets, hosts, relays, CSP sources) and Authenticators, responses as descriptions,
a security baseline on by default, Chord's own Node listener as a host-lifetime
object, nodeRequest for apps mounting it, links and plugin sockets.
chord/next/workers: a forked worker per address, started on demand, stopped when
idle, never restarted by itself, plugins built by the server and shipped.
Plugins: kinds with platform and host packages, createPluginRuntime, scripts(kind),
rollout results per host, imports of other plugins by name. Kernel: RouteHandle
(retry, suspend), no route backoff, anonymous host.root, origins only from relays.
ws is an optional peer dependency.
2026-10-31 20:40:36 +00:00

62 lines
2.7 KiB
TypeScript

// Tests of process locks (slice 10 §3): one holder at a time, a waiter taking the lock when the holder releases it
// (told by its connection's close, no polling), a waiter's abort.
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { BACKGROUND_CONTEXT as context, withAbortSignal } from "../../../src/context/index.ts";
import { processLock } from "../../../src/next-node/workers/index.ts";
import { lockAt } from "../../../src/next-node/workers/lock.ts";
describe("processLock", () => {
it("makes a second taker wait until the holder releases, then takes it", async () => {
const name = `chord-test-lock-${process.pid}-${Date.now()}`;
const first = await processLock(name, context);
let taken = false;
const second = processLock(name, context).then((lock) => {
taken = true;
return lock;
});
await new Promise((resolve) => setTimeout(resolve, 100));
expect(taken).toBe(false);
const released = Date.now();
await first.release();
await (await second).release();
expect(taken).toBe(true);
expect(Date.now() - released).toBeLessThan(100);
});
it("stops waiting when the waiter's context aborts", async () => {
const name = `chord-test-lock-abort-${process.pid}-${Date.now()}`;
const held = await processLock(name, context);
const abort = new AbortController();
const waiting = processLock(name, withAbortSignal(abort.signal, context));
abort.abort(new Error("gave up"));
await expect(waiting).rejects.toThrow("gave up");
await held.release();
await (await processLock(name, context)).release();
});
});
describe("socket file locks (macOS, the BSDs)", () => {
it("fail closed on a stale lock file, naming it: two takers never both take it over (Sol)", async () => {
const dir = await mkdtemp(join(tmpdir(), "chord-lock-file-"));
const address = join(dir, "lock.sock");
// What a crashed holder leaves: a path nobody accepts on.
await writeFile(address, "");
const takers = [lockAt({ kind: "file", address }, context), lockAt({ kind: "file", address }, context)];
for (const taker of takers) await expect(taker).rejects.toThrow(`lock: ${address} is a stale lock file`);
await rm(dir, { recursive: true, force: true });
});
it("are taken, waited for and released like the others", async () => {
const dir = await mkdtemp(join(tmpdir(), "chord-lock-file-"));
const address = join(dir, "lock.sock");
const first = await lockAt({ kind: "file", address }, context);
const second = lockAt({ kind: "file", address }, context);
await first.release();
await (await second).release();
await rm(dir, { recursive: true, force: true });
});
});