Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion extensions/relay/broker/supervisor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { constants } from "node:fs";
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { access, lstat, mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import lockfile from "proper-lockfile";

Expand Down Expand Up @@ -113,6 +113,23 @@ export async function ensureLocalBroker(options: EnsureLocalBrokerOptions): Prom
return { status: "started", paths, pid: started.pid };
}

async function removeLegacyBrokerLockFile(lockPath: string): Promise<void> {
try {
await unlink(lockPath);
} catch (error) {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;

// proper-lockfile owns directory locks. If unlink failed because this is already
// a directory, leave an active or stale directory lock for it to coordinate.
try {
if ((await lstat(lockPath)).isDirectory()) return;
} catch (statError) {
if (typeof statError === "object" && statError !== null && "code" in statError && statError.code === "ENOENT") return;
}
throw error;
}
}

export async function ensureScopedBroker(options: EnsureScopedBrokerOptions): Promise<EnsureLocalBrokerResult> {
const paths = brokerScopeControlPaths(options);
const isAlive = options.isAlive ?? isProcessAlive;
Expand All @@ -123,6 +140,10 @@ export async function ensureScopedBroker(options: EnsureScopedBrokerOptions): Pr
if (pid) return { status: "existing", paths, pid };
}

// Older releases used this path as a regular lock target and left the file
// behind. proper-lockfile now uses it as an atomic directory lock.
await removeLegacyBrokerLockFile(paths.lockPath);

const lockTargetPath = paths.lockPath.endsWith(".lock") ? paths.lockPath.slice(0, -".lock".length) : `${paths.lockPath}.target`;
const release = await lockfile.lock(lockTargetPath, {
lockfilePath: paths.lockPath,
Expand Down
25 changes: 24 additions & 1 deletion tests/relay/broker-supervisor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,30 @@ describe("local broker supervisor", () => {
const paths = brokerScopeControlPaths({ stateDir, tokenHash: "abc123" });
expect(starts).toBe(1);
expect(await readBrokerPid(paths.pidPath)).toBe(4444);
await expect(stat(`${paths.lockPath}.lock`)).rejects.toThrow();
await expect(stat(paths.lockPath)).rejects.toThrow();
});

it("replaces a legacy regular lock file with the directory lock format", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "pirelay-supervisor-"));
const paths = brokerScopeControlPaths({ stateDir, tokenHash: "abc123" });
await writeFile(paths.lockPath, "", { mode: 0o600 });
let observedDirectoryLock = false;

const result = await ensureScopedBroker({
stateDir,
tokenHash: "abc123",
startBroker: async () => {
observedDirectoryLock = (await stat(paths.lockPath)).isDirectory();
return { pid: 4567 };
},
probeSocket: async () => false,
waitForSocketReady: async () => undefined,
isAlive: () => false,
});

expect(result).toMatchObject({ status: "started", pid: 4567 });
expect(observedDirectoryLock).toBe(true);
await expect(stat(paths.lockPath)).rejects.toThrow();
});

it("does not return pid zero when a socket is ready before its pid file", async () => {
Expand Down