UNPKG

redis-smq-common

Version:

Provides essential components and utilities shared across RedisSMQ packages.

155 lines 5.59 kB
import { open, unlink, stat, utimes } from 'node:fs/promises'; import { dirname } from 'node:path'; import { env } from '../env/index.js'; import { AcquireLockError, AttemptsExhaustedError, ReleaseLockError, } from './errors/index.js'; function isAccessDeniedError(err) { return (err instanceof Error && 'code' in err && (err.code === 'EACCES' || err.code === 'EPERM')); } function isFileExistsError(err) { return err instanceof Error && 'code' in err && err.code === 'EEXIST'; } function isFileNotFoundError(err) { return err instanceof Error && 'code' in err && err.code === 'ENOENT'; } function getErrorMessage(err) { return err instanceof Error ? err.message : JSON.stringify(err); } export class FileLock { static activeLocks = new Map(); defaultRetryDelay = 1000; defaultRetries = 100; defaultStaleTimeout = 5 * 60 * 1000; defaultUpdateInterval = 60 * 1000; constructor() { this.setupCleanup(); } setupCleanup() { const cleanUp = async () => { for (const [lockFile, lockInfo] of FileLock.activeLocks.entries()) { try { if (lockInfo.updateInterval) { clearInterval(lockInfo.updateInterval); } await lockInfo.fileHandle.close(); await unlink(lockFile).catch(() => { }); } catch (err) { console.error(`Error releasing lock ${lockFile} during exit:`, err); } } FileLock.activeLocks.clear(); }; process.once('exit', cleanUp); process.once('SIGINT', cleanUp); process.once('SIGTERM', cleanUp); process.once('SIGUSR2', cleanUp); } async updateLockFileMtime(lockFile) { try { const now = new Date(); await utimes(lockFile, now, now); } catch (err) { if (isFileNotFoundError(err)) { const lockInfo = FileLock.activeLocks.get(lockFile); if (lockInfo?.updateInterval) { clearInterval(lockInfo.updateInterval); lockInfo.updateInterval = null; } } } } async isLockStale(lockFile, staleTimeout = this.defaultStaleTimeout) { try { const stats = await stat(lockFile); const now = Date.now(); const mtime = stats.mtime.getTime(); return now - mtime > staleTimeout; } catch (err) { if (isFileNotFoundError(err)) { return false; } return true; } } async acquireLock(lockFile, options = {}) { if (this.isLockHeld(lockFile)) { return; } const { retries = this.defaultRetries, retryDelay = this.defaultRetryDelay, staleTimeout = this.defaultStaleTimeout, updateInterval = this.defaultUpdateInterval, } = options; let attempts = 0; await env.ensureDirectoryExists(dirname(lockFile)); while (attempts < retries) { try { const fileHandle = await open(lockFile, 'wx'); const intervalId = setInterval(() => { this.updateLockFileMtime(lockFile).catch(() => { }); }, updateInterval); FileLock.activeLocks.set(lockFile, { fileHandle, updateInterval: intervalId, }); return; } catch (err) { if (isAccessDeniedError(err) || isFileExistsError(err)) { const isStale = await this.isLockStale(lockFile, staleTimeout); if (isStale) { try { await unlink(lockFile); continue; } catch (unlinkErr) { if (!isFileNotFoundError(unlinkErr)) { console.warn(`Failed to remove stale lock file ${lockFile}: ${getErrorMessage(unlinkErr)}`); } } } await new Promise((resolve) => setTimeout(resolve, retryDelay)); attempts++; continue; } throw new AcquireLockError({ metadata: { lockFile, error: getErrorMessage(lockFile), }, }); } } throw new AttemptsExhaustedError({ metadata: { lockFile, retries }, }); } async releaseLock(lockFile) { const lockInfo = FileLock.activeLocks.get(lockFile); if (!lockInfo) { return; } try { if (lockInfo.updateInterval) { clearInterval(lockInfo.updateInterval); } await lockInfo.fileHandle.close(); FileLock.activeLocks.delete(lockFile); await unlink(lockFile); } catch (err) { if (!isFileNotFoundError(err)) { throw new ReleaseLockError({ metadata: { lockFile, error: getErrorMessage(err), }, }); } } } isLockHeld(lockFile) { return FileLock.activeLocks.has(lockFile); } } //# sourceMappingURL=file-lock.js.map