async-lock vs lockfile vs proper-lockfile
File and Resource Locking Libraries
async-locklockfileproper-lockfileSimilar Packages:

File and Resource Locking Libraries

File and Resource Locking Libraries in Node.js are tools that help manage concurrent access to resources, such as files or critical sections of code, to prevent race conditions and ensure data integrity. These libraries implement locking mechanisms that allow only one process or thread to access a resource at a time, while others wait for their turn. This is particularly useful in scenarios where multiple processes may try to read from or write to the same resource simultaneously, leading to potential conflicts or data corruption. By using these libraries, developers can implement safe and reliable access control to shared resources in their applications.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
async-lock042618.3 kB52 years agoMIT
lockfile0259-128 years agoISC
proper-lockfile0271-215 years agoMIT

Feature Comparison: async-lock vs lockfile vs proper-lockfile

Locking Mechanism

  • async-lock:

    async-lock uses in-memory locks to prevent concurrent execution of asynchronous functions. It provides a simple API to create locks around specific code sections, ensuring that only one function can execute within a locked context at a time. This is particularly useful for preventing race conditions in applications that share resources in memory.

  • lockfile:

    lockfile creates file-based locks by creating a temporary lock file in the filesystem. When a process wants to acquire a lock on a resource, it attempts to create a lock file. If the file is created successfully, the process holds the lock; otherwise, it waits until the lock file is removed. This mechanism ensures that only one process can access the locked resource at a time, preventing conflicts and data corruption.

  • proper-lockfile:

    proper-lockfile also uses file-based locking but offers a more sophisticated approach. It creates a lock file with metadata, including the process ID (PID) of the locking process, which helps in identifying and managing locks more effectively. It supports advisory locking, allowing processes to cooperate and release locks gracefully. Additionally, it provides features to handle stale locks and ensure proper cleanup, making it more reliable for long-running applications.

Use Case

  • async-lock:

    async-lock is ideal for scenarios where you need to serialize access to shared resources in memory, such as when multiple asynchronous functions modify a shared variable or data structure. It is lightweight and easy to integrate into existing code without significant overhead.

  • lockfile:

    lockfile is suitable for scripts and applications that need to prevent concurrent modifications of files or directories, such as deployment scripts, backup utilities, or any process that requires exclusive access to a file. It is simple to use and does not require complex setup.

  • proper-lockfile:

    proper-lockfile is best for applications that require a more robust locking mechanism with better handling of edge cases. It is useful for long-running processes, daemons, or applications that need to manage locks across multiple instances and ensure proper cleanup of stale locks.

Error Handling

  • async-lock:

    async-lock provides basic error handling for lock acquisition and release. If a lock cannot be acquired (e.g., due to a timeout), it simply rejects the promise, allowing the caller to handle the error as needed. However, it does not provide built-in mechanisms for handling failed lock releases or detecting deadlocks.

  • lockfile:

    lockfile handles errors related to file operations, such as permission denied or file not found. If a lock cannot be acquired, it throws an error, which the caller must handle. The library does not include advanced error handling features, such as automatic recovery from stale locks or deadlock detection.

  • proper-lockfile:

    proper-lockfile offers more comprehensive error handling and recovery features. It provides detailed error messages and supports callbacks for handling lock acquisition and release events. The library includes mechanisms to detect and clean up stale locks, making it more resilient in the face of errors and unexpected process terminations.

Code Example

  • async-lock:

    async-lock Example

    const AsyncLock = require('async-lock');
    const lock = new AsyncLock();
    
    let sharedResource = 0;
    
    function updateResource() {
      return lock.acquire('key', async () => {
        const temp = sharedResource;
        await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate async work
        sharedResource = temp + 1;
      });
    }
    
    Promise.all([updateResource(), updateResource()]).then(() => {
      console.log(sharedResource); // Output: 2
    });
    
  • lockfile:

    lockfile Example

    const lockfile = require('lockfile');
    const path = require('path');
    
    const lockFilePath = path.join(__dirname, 'resource.lock');
    
    lockfile.lock(lockFilePath, { retries: 5, retryWait: 100 }, (err) => {
      if (err) return console.error('Failed to acquire lock:', err);
    
      // Perform operations on the locked resource
      console.log('Lock acquired. Performing operations...');
      setTimeout(() => {
        lockfile.unlock(lockFilePath, (err) => {
          if (err) return console.error('Failed to release lock:', err);
          console.log('Lock released.');
        });
      }, 2000);
    });
    
  • proper-lockfile:

    proper-lockfile Example

    const { lock, unlock } = require('proper-lockfile');
    const path = require('path');
    
    const lockFilePath = path.join(__dirname, 'resource.lock');
    
    async function performLockedOperation() {
      await lock(lockFilePath);
      try {
        console.log('Lock acquired. Performing operations...');
        await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate work
      } finally {
        await unlock(lockFilePath);
        console.log('Lock released.');
      }
    }
    
    performLockedOperation();
    

How to Choose: async-lock vs lockfile vs proper-lockfile

  • async-lock:

    Choose async-lock if you need a simple and lightweight solution for locking asynchronous code sections, especially when working with shared resources in memory. It is ideal for scenarios where you want to prevent concurrent execution of specific functions or code blocks within a single application instance.

  • lockfile:

    Choose lockfile if you need a straightforward way to create file-based locks to prevent multiple processes from modifying the same file or directory. It is suitable for scripts and applications that require a simple locking mechanism to ensure exclusive access to a file or resource.

  • proper-lockfile:

    Choose proper-lockfile if you need a more robust and feature-rich solution for file locking that supports multiple locking strategies, including advisory locks and ensures proper cleanup of locks. It is ideal for applications that require more control over the locking process and need to handle edge cases like crashes or unexpected terminations.

README for async-lock

async-lock

Lock on asynchronous code

Build Status

  • ES6 promise supported
  • Multiple keys lock supported
  • Timeout supported
  • Occupation time limit supported
  • Execution time limit supported
  • Pending task limit supported
  • Domain reentrant supported
  • 100% code coverage

Disclaimer

I did not create this package, and I will not add any features to it myself. I was granted the ownership because it was no longer being maintained, and I volunteered to fix a bug.

If you have a new feature you would like to have incorporated, please send me a PR and I will be happy to work with you and get it merged. For any bugs, PRs are most welcome but when possible I will try to get them resolved as soon as possible.

Why do you need locking on single threaded nodejs?

Nodejs is single threaded, and the code execution never gets interrupted inside an event loop, so locking is unnecessary? This is true ONLY IF your critical section can be executed inside a single event loop. However, if you have any async code inside your critical section (it can be simply triggered by any I/O operation, or timer), your critical logic will across multiple event loops, therefore it's not concurrency safe!

Consider the following code

redis.get('key', function(err, value) {
	redis.set('key', value * 2);
});

The above code simply multiply a redis key by 2. However, if two users run concurrently, the execution order may like this

user1: redis.get('key') -> 1
user2: redis.get('key') -> 1
user1: redis.set('key', 1 x 2) -> 2
user2: redis.set('key', 1 x 2) -> 2

Obviously it's not what you expected

With asyncLock, you can easily write your async critical section

lock.acquire('key', function(cb) {
	// Concurrency safe
	redis.get('key', function(err, value) {
		redis.set('key', value * 2, cb);
	});
}, function(err, ret) {
});

Get Started

var AsyncLock = require('async-lock');
var lock = new AsyncLock();

/**
 * @param {String|Array} key 	resource key or keys to lock
 * @param {function} fn 	execute function
 * @param {function} cb 	(optional) callback function, otherwise will return a promise
 * @param {Object} opts 	(optional) options
 */
lock.acquire(key, function(done) {
	// async work
	done(err, ret);
}, function(err, ret) {
	// lock released
}, opts);

// Promise mode
lock.acquire(key, function() {
	// return value or promise
}, opts).then(function() {
	// lock released
});

Error Handling

// Callback mode
lock.acquire(key, function(done) {
	done(new Error('error'));
}, function(err, ret) {
	console.log(err.message) // output: error
});

// Promise mode
lock.acquire(key, function() {
	throw new Error('error');
}).catch(function(err) {
	console.log(err.message) // output: error
});

Acquire multiple keys

lock.acquire([key1, key2], fn, cb);

Domain reentrant lock

Lock is reentrant in the same domain

var domain = require('domain');
var lock = new AsyncLock({domainReentrant : true});

var d = domain.create();
d.run(function() {
	lock.acquire('key', function() {
		//Enter lock
		return lock.acquire('key', function() {
			//Enter same lock twice
		});
	});
});

Options

// Specify timeout - max amount of time an item can remain in the queue before acquiring the lock
var lock = new AsyncLock({timeout: 5000});
lock.acquire(key, fn, function(err, ret) {
	// timed out error will be returned here if lock not acquired in given time
});

// Specify max occupation time - max amount of time allowed between entering the queue and completing execution
var lock = new AsyncLock({maxOccupationTime: 3000});
lock.acquire(key, fn, function(err, ret) {
	// occupation time exceeded error will be returned here if job not completed in given time
});

// Specify max execution time - max amount of time allowed between acquiring the lock and completing execution
var lock = new AsyncLock({maxExecutionTime: 3000});
lock.acquire(key, fn, function(err, ret) {
	// execution time exceeded error will be returned here if job not completed in given time
});

// Set max pending tasks - max number of tasks allowed in the queue at a time
var lock = new AsyncLock({maxPending: 1000});
lock.acquire(key, fn, function(err, ret) {
	// Handle too much pending error
})

// Whether there is any running or pending async function
lock.isBusy();

// Use your own promise library instead of the global Promise variable
var lock = new AsyncLock({Promise: require('bluebird')}); // Bluebird
var lock = new AsyncLock({Promise: require('q')}); // Q

// Add a task to the front of the queue waiting for a given lock
lock.acquire(key, fn1, cb); // runs immediately
lock.acquire(key, fn2, cb); // added to queue
lock.acquire(key, priorityFn, cb, {skipQueue: true}); // jumps queue and runs before fn2

Changelog

See Changelog

Issues

See issue tracker.

License

MIT, see LICENSE