Inter-Process Lockfile Utility
proper-lockfile is a robust JavaScript utility for managing inter-process and inter-machine file locks across local and network file systems. Currently at version 4.1.2, it is actively maintained with updates released as needed. Its core design uses an atomic `mkdir` strategy for lockfile creation, which is more reliable than `open` with `O_EXCL` flags, especially on network file systems (NFS) where `O_EXCL` is prone to race conditions. The library differentiates itself by constantly updating the lockfile's `mtime` (modified time) to accurately check for staleness, a significant improvement over `ctime` (creation time) for long-running processes. Furthermore, it incorporates mechanisms to detect when a lockfile might be compromised due to failed updates or unexpected delays, enhancing overall reliability compared to alternatives.
Common errors
-
Error: Lock file is already being held
cause Another process currently holds the lock, and the current attempt to acquire it did not succeed within the configured retries/timeout.fixHandle the `ELOCKED` error code (if available) or the general error by implementing retry logic with exponential backoff or waiting for the lock to be released. Configure the `retries` option for `lock()`. -
Error: Lock was already compromised
cause The lockfile's integrity check failed during a background update, or it was manually removed, indicating the lock is no longer reliably held by this process. This error can be thrown by the `onCompromised` callback or when `release()` is called on a compromised lock.fixImplement robust error handling in the `onCompromised` callback and for the `release()` promise. This usually indicates a critical state requiring process termination or immediate re-evaluation of resource access. -
Error: ENOENT: no such file or directory, lstat 'path/to/file.txt.lock'
cause The base file (e.g., 'path/to/file.txt') did not exist when `lock()` or `check()` was called, and `realpath` option is true (default). `proper-lockfile` expects the target file to exist to resolve its real path and create the `.lock` file alongside it.fixEnsure the target file you intend to lock exists before calling `lock()` or `check()`. Use `fs.promises.writeFile(filePath, '', { flag: 'a+' })` to create it if it doesn't exist.
Warnings
- gotcha Using different `stale` or `update` option values for the same file across different processes can lead to race conditions and multiple processes acquiring what they believe is an exclusive lock.
- gotcha Manual removal of a lockfile by an external process or user can lead to the lock being compromised. `proper-lockfile` detects certain compromises (failed updates) but cannot detect external, arbitrary deletions which would allow another process to acquire a lock immediately after.
- gotcha The `release()` function returned by `lock()` can reject its promise if the lock has been compromised (e.g., updates failed, or it became stale). Consumers must handle this rejection to prevent unhandled promise rejections or misinterpretations of lock status.
- gotcha The default `stale` option is 10 seconds (10000ms), with a minimum of 5 seconds (5000ms). Setting `stale` too low for long-running operations or processes with high latency can cause locks to be considered stale prematurely.
Install
-
npm install proper-lockfile -
yarn add proper-lockfile -
pnpm add proper-lockfile
Imports
- lock
const lock = require('proper-lockfile').lock;import { lock } from 'proper-lockfile'; - unlock
const unlock = require('proper-lockfile').unlock;import { unlock } from 'proper-lockfile'; - check
const check = require('proper-lockfile').check;import { check } from 'proper-lockfile';
Quickstart
import { lock, unlock } from 'proper-lockfile';
import { promises as fs } from 'fs';
import path from 'path';
const filePath = path.join(process.cwd(), 'my-resource.txt');
const lockFileOptions = {
stale: 15000, // Consider lock stale after 15 seconds
update: 5000, // Update mtime every 5 seconds
retries: {
retries: 5,
factor: 2,
minTimeout: 1000,
maxTimeout: 10000,
randomize: true,
},
onCompromised: (err) => {
console.error('Lock was compromised:', err.message);
// Implement critical error handling here, e.g., exit process
process.exit(1);
},
};
async function accessResourceWithLock() {
let release;
try {
// Ensure the target file exists before trying to lock it
await fs.writeFile(filePath, 'Initial content.', { flag: 'a+' });
console.log('Attempting to acquire lock...');
release = await lock(filePath, lockFileOptions);
console.log('Lock acquired. Performing sensitive operation...');
// Simulate work
await new Promise(resolve => setTimeout(resolve, Math.random() * 3000 + 1000));
await fs.appendFile(filePath, `\nAccessed at ${new Date().toISOString()} by process ${process.pid}`);
console.log('Sensitive operation complete. Releasing lock...');
} catch (error) {
console.error('Failed to acquire or release lock:', error.message);
if (error.code === 'ELOCKED') {
console.warn('File is already locked by another process.');
}
} finally {
if (release) {
try {
await release();
console.log('Lock released successfully.');
} catch (error) {
console.error('Error releasing lock:', error.message);
}
} else {
// If lock was never acquired, or release failed (e.g. compromised), ensure cleanup.
// In a real scenario, you might also attempt `unlock(filePath)` here if `release` failed
// but only if you are confident it's safe to force-unlock.
console.log('No release function available or lock acquisition failed. No explicit release needed/possible.');
}
}
}
accessResourceWithLock();