redis-smq-common
Version:
RedisSMQ Common Library provides many components that are mainly used by RedisSMQ and RedisSMQ Monitor.
143 lines • 5.35 kB
JavaScript
import { open, unlink, stat, utimes } from 'node:fs/promises';
import { dirname } from 'node:path';
import { env } from '../env/index.js';
import { FileLockAttemptsExhaustedError, FileLockError, } 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 FileLockError(`Failed to acquire lock (${lockFile}): ${getErrorMessage(err)}`);
}
}
throw new FileLockAttemptsExhaustedError(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 FileLockError(`Failed to release lock on (${lockFile}): ${getErrorMessage(err)}`);
}
}
}
isLockHeld(lockFile) {
return FileLock.activeLocks.has(lockFile);
}
}
//# sourceMappingURL=file-lock.js.map