Skip to content

Commit

Permalink
Merge pull request #377 from kleros/refactor/lockfile
Browse files Browse the repository at this point in the history
refactor/lockfile
  • Loading branch information
mani99brar authored Dec 13, 2024
2 parents cbfe0a3 + 3d6075c commit 5041531
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 11 deletions.
84 changes: 84 additions & 0 deletions relayer-cli/src/utils/lock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { claimLock, getLockFilePath, LockfileExistsError, releaseLock } from "./lock";

describe("Lock", () => {
describe("getLockFilePath", () => {
it("should return the lock file path for a given network and chain id", () => {
const network = "mainnet";
const chainId = 1;

const result = getLockFilePath(network, chainId);
expect(result).toBe("./state/mainnet_1.pid");
});

it("should ensure the network name is lowercase", () => {
const network = "MAINNET";
const chainId = 1;

const result = getLockFilePath(network, chainId);
expect(result).toBe("./state/mainnet_1.pid");
});
});

describe("claimLock", () => {
const network = "mainnet";
const chainId = 1;
const expectedLockFilePath = getLockFilePath(network, chainId);

it("should throw an error if the lockfile already exists", () => {
const deps = {
fileExistsFn: jest.fn().mockReturnValue(true),
};

expect(() => claimLock(network, chainId, deps)).toThrow(LockfileExistsError);
});

it("should write a file with the PID if none exists", () => {
const deps = {
fileExistsFn: jest.fn().mockReturnValue(false),
writeFileFn: jest.fn(),
};

claimLock(network, chainId, deps);

expect(deps.fileExistsFn).toHaveBeenCalledTimes(1);
expect(deps.writeFileFn).toHaveBeenCalledTimes(1);

const [path, pid] = deps.writeFileFn.mock.calls[0];
expect(path).toBe(expectedLockFilePath);
expect(pid).toBe(process.pid.toString());
});
});

describe("releaseLock", () => {
const network = "mainnet";
const chainId = 1;
const expectedLockFilePath = getLockFilePath(network, chainId);

it("should remove the lockfile if it exists", () => {
const deps = {
fileExistsFn: jest.fn().mockReturnValue(true),
unlinkFileFn: jest.fn(),
};

releaseLock(network, chainId, deps);

expect(deps.fileExistsFn).toHaveBeenCalledTimes(1);
expect(deps.unlinkFileFn).toHaveBeenCalledTimes(1);

const [path] = deps.unlinkFileFn.mock.calls[0];
expect(path).toBe(expectedLockFilePath);
});

it("should do nothing if the file does not exist", () => {
const deps = {
fileExistsFn: jest.fn().mockReturnValue(false),
unlinkFileFn: jest.fn(),
};

releaseLock(network, chainId, deps);

expect(deps.fileExistsFn).toHaveBeenCalledTimes(1);
expect(deps.unlinkFileFn).not.toHaveBeenCalled();
});
});
});
85 changes: 85 additions & 0 deletions relayer-cli/src/utils/lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fs from "fs";

/**
* Returns the lock file path for a given network and chain id
*
* @param network - The network name
* @param chainId - The numerical identifier of the chain
* @returns The lock file path
*
* @example
* getLockFilePath('goerli', 1); // './state/goerli_1.pid'
*/
export function getLockFilePath(network: string, chainId: number) {
return `./state/${network.toLowerCase()}_${chainId}.pid`;
}

export class LockfileExistsError extends Error {
constructor(path: string) {
super();
this.message = `The application tried to claim the lockfile ${path} but it already exists. Please ensure no other instance is running and delete the lockfile before starting a new one.`;
this.name = "OnlyOneProcessError";
}
}

type ClaimLockDependencies = {
fileExistsFn?: typeof fs.existsSync;
writeFileFn?: typeof fs.writeFileSync;
};

/**
* Ensures there is only one process running at the same time for a given lock file.
*
* If the lock file exists, thrown an error. If it does not exists, creates it with the current process id.
*
* @param network - The network name
* @param chain - The chain id
* @param dependencies - FS methods to be used
*
* @example
* claimLock('/opt/app/lock.pid');
*/
export function claimLock(
network: string,
chain: number,
dependencies: ClaimLockDependencies = {
fileExistsFn: fs.existsSync,
writeFileFn: fs.writeFileSync,
}
) {
const path = getLockFilePath(network, chain);
const { fileExistsFn, writeFileFn } = dependencies;

if (fileExistsFn(path)) throw new LockfileExistsError(path);
writeFileFn(path, process.pid.toString(), { encoding: "utf8" });
}

type ReleaseLockDependencies = {
fileExistsFn?: typeof fs.existsSync;
unlinkFileFn?: typeof fs.unlinkSync;
};

/**
* Ensures the lock file is removed
*
* @param network - The network name
* @param chainId - The numerical identifier of the chain
* @param dependencies - FS methods to be used
*
* @example
* releaseLock('/opt/app/lock.pid');
*/
export function releaseLock(
network: string,
chain: number,
dependencies: ReleaseLockDependencies = {
fileExistsFn: fs.existsSync,
unlinkFileFn: fs.unlinkSync,
}
) {
const { fileExistsFn, unlinkFileFn } = dependencies;
const path = getLockFilePath(network, chain);

if (!fileExistsFn(path)) return;
unlinkFileFn(path);
}
14 changes: 3 additions & 11 deletions relayer-cli/src/utils/relayerHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import * as fs from "fs";
import { claimLock, releaseLock } from "./lock";
import ShutdownManager from "./shutdownManager";

async function initialize(chainId: number, network: string): Promise<number> {
const lockFileName = "./state/" + network + "_" + chainId + ".pid";

if (fs.existsSync(lockFileName)) {
console.log("Skipping chain with process already running, delete pid file to force", chainId);
throw new Error("Already running");
}
fs.writeFileSync(lockFileName, process.pid.toString(), { encoding: "utf8" });
claimLock(network, chainId);

// STATE_DIR is absolute path of the directory where the state files are stored
// STATE_DIR must have trailing slash
Expand Down Expand Up @@ -39,10 +34,7 @@ async function updateStateFile(chainId: number, createdTimestamp: number, nonceF
};
fs.writeFileSync(chain_state_file, JSON.stringify(json), { encoding: "utf8" });

const lockFileName = "./state/" + network + "_" + chainId + ".pid";
if (fs.existsSync(lockFileName)) {
fs.unlinkSync(lockFileName);
}
releaseLock(network, chainId);
}

async function setupExitHandlers(chainId: number, shutdownManager: ShutdownManager, network: string) {
Expand Down

0 comments on commit 5041531

Please sign in to comment.