In the world of asynchronous programming with Node.js, managing concurrent operations can be a significant challenge. Race conditions, where the outcome of a program depends on the unpredictable sequence or timing of events, can lead to unexpected bugs and data corruption. This is where race-lock-js comes in – a lightweight, in-memory lock utility designed to help you safely coordinate asynchronous operations within a single Node.js process.
What is RaceLock JS?
race-lock-js (version 0.0.4) is a simple yet powerful library that provides an in-memory locking mechanism to prevent race conditions. It acts as a mutex, ensuring that only one operation can access a critical section of code at a time, thereby maintaining data integrity and predictable behavior in your asynchronous workflows.
Key Features:
Simple API: Easy-to-use start() and end() methods for acquiring and releasing locks.
Auto-release with Timeouts: Optional timeouts ensure locks are not held indefinitely.
Ownership Checks: Prevents unintended release of locks by different operations.
Exponential Backoff Retry: retryStart() offers a robust way to acquire locks under contention.
waitForUnlock(): Allows operations to pause and wait until a specific lock is released.
Metadata Support: Attach contextual information to your locks.
Introspection: Utilities like getLockCount(), getAllLockedKeys(), and getLockInfo() for monitoring.
Emergency Unlocking: forceUnlock() and clearAllLocks() for critical situations (use with caution).
Installation
Getting started with race-lock-js is straightforward. You can install it via npm:
Bash
npm install racelock
How to Use RaceLock JS: Examples and Demo Code
Let's dive into some practical examples to understand how to leverage race-lock-js in your applications.
First, import the library:
JavaScript
const RaceLock = require('racelock');
const lock = new RaceLock();
Basic Locking
This demonstrates how to acquire and release a lock for a simple asynchronous task.
JavaScript
async function performCriticalTask(taskId) {
const lockKey = `task-${taskId}-lock`;
if (lock.start(lockKey)) {
try {
console.log(`Task ${taskId}: Lock acquired for ${lockKey}. Performing critical operation...`);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Task ${taskId}: Critical operation complete.`);
} finally {
lock.end(lockKey);
console.log(`Task ${taskId}: Lock released for ${lockKey}.`);
}
} else {
console.log(`Task ${taskId}: Could not acquire lock for ${lockKey}. Already locked.`);
}
}
performCriticalTask(1);
performCriticalTask(1);
performCriticalTask(2);
Asynchronous Task Protection with Timeouts
You can set a timeout for a lock to automatically release if it's held for too long.
JavaScript
async function sensitiveOperation(userId) {
const lockKey = `user-${userId}-data`;
const timeoutMs = 2000;
if (lock.start(lockKey, timeoutMs, { userId: userId, operation: 'updateProfile' })) {
try {
console.log(`User ${userId}: Lock acquired with timeout. Updating profile...`);
await new Promise(resolve => setTimeout(resolve, 1500));
console.log(`User ${userId}: Profile updated successfully.`);
} finally {
lock.end(lockKey);
console.log(`User ${userId}: Lock released.`);
}
} else {
console.log(`User ${userId}: Failed to acquire lock for profile update.`);
}
}
sensitiveOperation(123);
sensitiveOperation(123);
Using retryStart() for Contention
retryStart() is excellent for scenarios where multiple operations might contend for the same lock. It retries acquiring the lock with an exponential backoff.
JavaScript
async function processOrder(orderId) {
const lockKey = `order-${orderId}-processing`;
const ownerId = `processor-${Math.random().toFixed(4)}`;
try {
const acquired = await lock.retryStart(
lockKey,
5,
100,
3000,
{ orderId: orderId, source: 'web' },
ownerId
);
if (acquired) {
console.log(`${ownerId} for Order ${orderId}: Lock acquired. Processing order...`);
await new Promise(resolve => setTimeout(resolve, 2500));
console.log(`${ownerId} for Order ${orderId}: Order processed.`);
} else {
console.error(`${ownerId} for Order ${orderId}: Failed to acquire lock after multiple attempts.`);
}
} catch (error) {
console.error(`${ownerId} for Order ${orderId}: Error during lock acquisition or processing:`, error.message);
} finally {
if (lock.isLocked(lockKey) && lock.getLockInfo(lockKey)?.ownerId === ownerId) {
lock.end(lockKey, ownerId);
console.log(`${ownerId} for Order ${orderId}: Lock released.`);
}
}
}
processOrder(101);
processOrder(101);
processOrder(102);
Waiting for a Lock to be Released with waitForUnlock()
If an operation needs to wait until a specific lock is free, waitForUnlock() is the perfect solution.
JavaScript
async function consumerProcess(dataKey) {
console.log(`Consumer for ${dataKey}: Checking if lock is active...`);
while (lock.isLocked(dataKey)) {
console.log(`Consumer for ${dataKey}: Lock is active, waiting...`);
await lock.waitForUnlock(dataKey);
}
console.log(`Consumer for ${dataKey}: Lock is no longer active. Proceeding with data processing.`);
await new Promise(resolve => setTimeout(resolve, 500));
console.log(`Consumer for ${dataKey}: Data processing complete.`);
}
async function producerProcess(dataKey) {
const ownerId = 'producer-A';
if (lock.start(dataKey, 0, {}, ownerId)) {
try {
console.log(`Producer for ${dataKey}: Lock acquired. Producing data...`);
await new Promise(resolve => setTimeout(resolve, 3000));
console.log(`Producer for ${dataKey}: Data production complete.`);
} finally {
lock.end(dataKey, ownerId);
console.log(`Producer for ${dataKey}: Lock released.`);
}
}
}
const sharedDataKey = 'mySharedResource';
consumerProcess(sharedDataKey);
setTimeout(() => producerProcess(sharedDataKey), 500);
Best Practices for Using race-lock-js
Always Use finally Blocks: Ensure lock.end() is called within a finally block to guarantee lock release, even if errors occur.
Utilize ownerId: When multiple systems or functions might interact with the same lock key, use ownerId to enforce ownership and prevent accidental releases.
Employ retryStart() for Contention: If you anticipate contention for a lock, retryStart() provides a robust mechanism to acquire it gracefully.
Use clearAllLocks() with Extreme Caution: This function clears all active locks and should only be used in emergency scenarios or for testing, as it can disrupt ongoing operations.
License and Contributions
race-lock-js is open-source, licensed under the MIT License. Contributions are welcome! You can contribute by forking the repository, starring it, submitting pull requests, or opening issues on its GitHub repository.
For more details, you can visit the npm package page directly.