Skip to main content

Command Palette

Search for a command to run...

Understanding and Using race-lock-js: A Guide to Preventing Race Conditions

Updated
5 min read

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`;

    // Try to acquire the lock
    if (lock.start(lockKey)) {
        try {
            console.log(`Task ${taskId}: Lock acquired for ${lockKey}. Performing critical operation...`);
            // Simulate an asynchronous operation
            await new Promise(resolve => setTimeout(resolve, 1000));
            console.log(`Task ${taskId}: Critical operation complete.`);
        } finally {
            // Always ensure the lock is released
            lock.end(lockKey);
            console.log(`Task ${taskId}: Lock released for ${lockKey}.`);
        }
    } else {
        console.log(`Task ${taskId}: Could not acquire lock for ${lockKey}. Already locked.`);
    }
}

// Simulate multiple tasks trying to access the same resource
performCriticalTask(1);
performCriticalTask(1); // This will likely fail to acquire the lock initially
performCriticalTask(2); // This will acquire a different lock

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; // Lock will auto-release after 2 seconds

    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)); // Simulate a task that finishes within timeout
            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);
// If called again immediately, it might fail or wait for the timeout/release
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)}`; // Unique ID for this attempt

    try {
        const acquired = await lock.retryStart(
            lockKey,
            5, // Number of attempts
            100, // Initial delay (ms)
            3000, // Timeout for each lock acquisition attempt (ms)
            { orderId: orderId, source: 'web' }, // Metadata
            ownerId
        );

        if (acquired) {
            console.log(`${ownerId} for Order ${orderId}: Lock acquired. Processing order...`);
            await new Promise(resolve => setTimeout(resolve, 2500)); // Simulate order processing
            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 {
        // Ensure lock is released by its owner
        if (lock.isLocked(lockKey) && lock.getLockInfo(lockKey)?.ownerId === ownerId) {
            lock.end(lockKey, ownerId);
            console.log(`${ownerId} for Order ${orderId}: Lock released.`);
        }
    }
}

// Simulate multiple attempts to process the same order
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.`);
    // Now you can safely access or modify the data
    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)); // Simulate data production
            console.log(`Producer for ${dataKey}: Data production complete.`);
        } finally {
            lock.end(dataKey, ownerId);
            console.log(`Producer for ${dataKey}: Lock released.`);
        }
    }
}

const sharedDataKey = 'mySharedResource';

// Start consumer first, it will wait
consumerProcess(sharedDataKey);
// Start producer after a short delay, it will acquire the lock
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.

Understanding and Using race-lock-js: A Guide to Preventing Race Conditions