From 5ee52e6d2300f71a6cac467bf0b4d1bc32927a20 Mon Sep 17 00:00:00 2001 From: NeuralFlux <40491005+NeuralFlux@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:51:15 -0500 Subject: [PATCH 1/2] fix: add file locking mechanism --- package.json | 3 ++- src/misc.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) 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..29e2290 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,3 +1,6 @@ +import lockfile from "proper-lockfile"; +import { setTimeout as sleep } from "timers/promises"; + export function toArray(input: Type | Type[]): Type[] { if (Array.isArray(input)) { return input; @@ -76,3 +79,62 @@ 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(filePath: string, action: () => Promise, debug?: (message: string) => void): Promise { + if (process.env.NODE_ENV !== "production") { + debug(`Development mode: Skipping lockfile ${process.env.NODE_ENV}`); + const result = await action(); + return result; + } + + let release; + try { + release = await lockfile.lock(filePath, LOCKFILE_RETRY_CONFIG); + const result = await action(); + return result; + } catch (error) { + debug(`Lockfile error: ${error}`); + // throw error; + } finally { + if (release) release(); + } +} + +export function lockWithActionSync(filePath: string, action: () => T, debug?: (message: string) => void): T { + if (process.env.NODE_ENV !== "production") { + debug(`Development mode: Skipping lockfile ${process.env.NODE_ENV}`); + return action(); + } + + let release; + try { + const startTime = Date.now(); + + while (Date.now() - startTime < LOCKFILE_STALENESS["stale"]) { + if (!lockfile.checkSync(filePath)) { + release = lockfile.lockSync(filePath, LOCKFILE_STALENESS); + const result = action(); + return result; + } else { + sleep(LOCKFILE_RETRY_CONFIG["retries"]["minTimeout"]); + } + } + debug("Lockfile timeout: did not read file"); + } catch (error) { + debug(`Lockfile error: ${error}`); + // throw error; + } finally { + if (release) release(); + } +} From 55c6640ff96629494ab81bb6cc5da3e9c416cae4 Mon Sep 17 00:00:00 2001 From: NeuralFlux <40491005+NeuralFlux@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:35:58 -0500 Subject: [PATCH 2/2] fix: remove sync lock and add simultaneous async locks --- src/misc.ts | 42 +++++++++--------------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/src/misc.ts b/src/misc.ts index 29e2290..a318038 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,5 +1,4 @@ import lockfile from "proper-lockfile"; -import { setTimeout as sleep } from "timers/promises"; export function toArray(input: Type | Type[]): Type[] { if (Array.isArray(input)) { @@ -91,50 +90,27 @@ export const LOCKFILE_RETRY_CONFIG = { stale: LOCKFILE_STALENESS["stale"], }; -export async function lockWithActionAsync(filePath: string, action: () => Promise, debug?: (message: string) => void): Promise { +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; } - let release; + const releases: (() => void)[] = []; + const retryConfig = lockfileRetryConfig || LOCKFILE_RETRY_CONFIG; try { - release = await lockfile.lock(filePath, LOCKFILE_RETRY_CONFIG); + 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 { - if (release) release(); - } -} - -export function lockWithActionSync(filePath: string, action: () => T, debug?: (message: string) => void): T { - if (process.env.NODE_ENV !== "production") { - debug(`Development mode: Skipping lockfile ${process.env.NODE_ENV}`); - return action(); - } - - let release; - try { - const startTime = Date.now(); - - while (Date.now() - startTime < LOCKFILE_STALENESS["stale"]) { - if (!lockfile.checkSync(filePath)) { - release = lockfile.lockSync(filePath, LOCKFILE_STALENESS); - const result = action(); - return result; - } else { - sleep(LOCKFILE_RETRY_CONFIG["retries"]["minTimeout"]); - } - } - debug("Lockfile timeout: did not read file"); - } catch (error) { - debug(`Lockfile error: ${error}`); - // throw error; - } finally { - if (release) release(); + for (const release of releases) + if (release) release(); } }