redis-rwlock
v1.0.0
Published
Distributed read/write locks with Redis - queue-based, no polling, immediate acquisition
Maintainers
Readme
redis-rwlock
A production-ready distributed read/write lock library for Node.js using Redis. Features queue-based waiting with immediate lock acquisition on release (no polling or retrying).
Features
- 🔐 Read/Write Locks - Multiple readers OR single writer
- 📋 Queue-Based Waiting - FIFO ordering, no polling needed
- ⚡ Immediate Acquisition - Waiters notified instantly when lock is released
- ⏱️ Per-Acquisition Timeouts - Customize lock duration and wait timeout per call
- 🔄 Lock Extension - Extend lock duration while holding it
- ✍️ Writer Priority - Prevent writer starvation (configurable)
- 🛡️ Auto-Release - Locks expire automatically to prevent deadlocks
- 🔌 Connection Recovery - Handles Redis reconnection gracefully
- 📦 TypeScript - Full type definitions included
Installation
npm install redis-rwlock ioredisQuick Start
import Redis from 'ioredis';
import { createLockManager } from 'redis-rwlock';
const redis = new Redis();
const lockManager = createLockManager(redis);
// Acquire a write lock
const lock = await lockManager.acquire('my-resource', 'write');
try {
// Critical section - you have exclusive access
await updateDatabase();
} finally {
await lock.release();
}API
Creating a Lock Manager
import { createLockManager } from 'redis-rwlock';
const lockManager = createLockManager(redis, {
keyPrefix: 'rwlock', // Redis key prefix (default: 'rwlock')
defaultLockDuration: 30000, // Default lock TTL in ms (default: 30000)
defaultWaitTimeout: 10000, // Default max wait time in ms (default: 10000)
writerPriority: true, // Block new readers when writer waiting (default: true)
cleanupInterval: 30000, // Background cleanup interval (default: 30000)
debug: false, // Enable debug logging (default: false)
});Acquiring Locks
// Write lock (exclusive)
const writeLock = await lockManager.acquire('resource', 'write');
// Read lock (shared)
const readLock = await lockManager.acquire('resource', 'read');
// With custom options
const lock = await lockManager.acquire('resource', 'write', {
lockDuration: 60000, // Hold for 60 seconds
waitTimeout: 5000, // Wait max 5 seconds
onQueued: (position) => console.log(`Queued at position ${position}`),
});Try Without Waiting
const lock = await lockManager.tryAcquire('resource', 'write');
if (lock) {
// Got the lock
await lock.release();
} else {
// Lock not available
}Lock Operations
// Check if still valid
if (lock.isValid()) {
// Still holding the lock
}
// Get remaining time
const remainingMs = lock.remainingTime();
// Extend the lock
const newExpiry = await lock.extend(30000); // Add 30 seconds
// Release the lock
await lock.release();Lock Information
// Check if locked
const isLocked = await lockManager.isLocked('resource');
// Get detailed info
const info = await lockManager.getLockInfo('resource');
// Returns: { type, holders, expiresAt, queueLength, hasWriterWaiting }
// Get queue length
const queueLength = await lockManager.getQueueLength('resource');Shutdown
// Releases all locks and cleans up
await lockManager.shutdown();Error Handling
import {
AcquisitionTimeoutError,
NotHeldError,
LockExpiredError
} from 'redis-rwlock';
try {
const lock = await lockManager.acquire('resource', 'write', {
waitTimeout: 1000,
});
} catch (error) {
if (error instanceof AcquisitionTimeoutError) {
console.log(`Timed out waiting for ${error.resource}`);
}
}How It Works
Queue-Based Architecture
Unlike traditional lock implementations that use polling/retrying, redis-rwlock uses:
- Sorted Sets for the waiter queue (score = arrival time for FIFO)
- Pub/Sub for instant notification when locks are released
- Lua Scripts for atomic operations
When a lock is released:
- The release script atomically grants the lock to the next waiter(s)
- Notifications are published to wake up the waiters
- Waiters verify their status and return the lock handle
Read/Write Semantics
- Read locks are shared - multiple readers can hold simultaneously
- Write locks are exclusive - only one writer, no readers
- When a write lock is released, consecutive read locks at the queue head are granted together
Writer Priority
With writerPriority: true (default):
- Once a writer is waiting, new readers must queue behind it
- Prevents writer starvation in read-heavy workloads
Best Practices
Always Use try/finally
const lock = await lockManager.acquire('resource', 'write');
try {
await doWork();
} finally {
await lock.release();
}Set Appropriate Timeouts
// Short operations: shorter lock duration
const lock = await lockManager.acquire('resource', 'write', {
lockDuration: 5000, // 5 seconds max
waitTimeout: 2000, // Don't wait too long
});
// Long operations: longer duration, extend if needed
const lock = await lockManager.acquire('resource', 'write', {
lockDuration: 60000,
});
// Extend before expiring
if (lock.remainingTime() < 10000) {
await lock.extend(30000);
}Handle Timeouts Gracefully
try {
const lock = await lockManager.acquire('resource', 'write', {
waitTimeout: 1000,
});
// ... use lock
} catch (error) {
if (error instanceof AcquisitionTimeoutError) {
// Return 503 or retry later
return res.status(503).json({ error: 'Resource busy' });
}
throw error;
}Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT © [Your Name]
