diff --git a/package.json b/package.json index 66a64a4..a33e30b 100644 --- a/package.json +++ b/package.json @@ -31,13 +31,13 @@ "devDependencies": { "@commitlint/cli": "^18.2.0", "@commitlint/config-conventional": "^11.0.0", - "biolink-model": "workspace:../biolink-model", "@types/debug": "^4.1.10", "@types/jest": "^29.5.7", "@types/lodash": "^4.14.200", "@types/node": "^20.8.10", "@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/parser": "^6.8.0", + "biolink-model": "workspace:../biolink-model", "eslint": "^8.53.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.1", @@ -59,6 +59,7 @@ "ioredis": "^5.3.2", "ioredis-mock": "^8.9.0", "lodash": "^4.17.21", + "proper-lockfile": "^4.1.2", "redlock": "5.0.0-beta.2" } } diff --git a/src/misc.ts b/src/misc.ts index b615233..a318038 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,3 +1,5 @@ +import lockfile from "proper-lockfile"; + export function toArray(input: Type | Type[]): Type[] { if (Array.isArray(input)) { return input; @@ -76,3 +78,39 @@ export function timeoutPromise(promise: Promise, timeout: number): Promise reject = newReject; }); } + +export const LOCKFILE_STALENESS = {stale: 5000}; // lock expiration in milliseconds to prevent deadlocks +export const LOCKFILE_RETRY_CONFIG = { + retries: { + retries: 10, + factor: 2, + minTimeout: 100, + maxTimeout: 1000, + }, + stale: LOCKFILE_STALENESS["stale"], +}; + +export async function lockWithActionAsync(filePaths: string[], action: () => Promise, debug?: (message: string) => void, lockfileRetryConfig?: any): Promise { + if (process.env.NODE_ENV !== "production") { + debug(`Development mode: Skipping lockfile ${process.env.NODE_ENV}`); + const result = await action(); + return result; + } + + const releases: (() => void)[] = []; + const retryConfig = lockfileRetryConfig || LOCKFILE_RETRY_CONFIG; + try { + for (const filePath of filePaths) { + let release = await lockfile.lock(filePath, retryConfig); + releases.push(release); + } + const result = await action(); + return result; + } catch (error) { + debug(`Lockfile error: ${error}`); + // throw error; + } finally { + for (const release of releases) + if (release) release(); + } +}