@justin-kucerak/redlock v1.0.0
Redlock - Distributed Locking with Redis in TypeScript
A TypeScript implementation of the Redlock algorithm for distributed locking using Redis. This package provides a flexible, client-agnostic locking mechanism suitable for synchronization across multiple processes or services.
Table of Contents
- Features
- Installation
- Usage
- API Documentation
- Implementing Redis Clients
- Testing
- Contributing
- License
- Acknowledgments
Features
- Distributed Locking: Implements the Redlock algorithm for safe distributed locking across multiple Redis instances.
- Client-Agnostic: Works with any Redis client library that implements the provided
RedisClientinterface. - Event-Driven: Emits events for lock acquisition, release, extension, and errors, allowing for custom handling and logging.
- TypeScript Support: Fully typed for enhanced development experience and code safety.
Installation
Install the package via npm:
npm install @justin-kucerak/redlockYou'll also need to install a Redis client library. The package supports ioredis and node-redis via provided adapters.
For ioredis:
npm install ioredisFor node-redis:
npm install redisNote: The Redis client libraries are listed as peer dependencies. You must install one of them separately.
Usage
Quick Start
Here's a simple example of how to use the Redlock class with ioredis:
import { Redlock, IORedisAdapter } from '@justin-kucerak/redlock';
import Redis from 'ioredis';
// Create Redis clients
const redisClients = [
new Redis({ host: 'localhost', port: 6379 }),
new Redis({ host: 'localhost', port: 6380 }),
new Redis({ host: 'localhost', port: 6381 }),
];
// Wrap clients with adapters
const clients = redisClients.map((client) => new IORedisAdapter(client));
// Create Redlock instance
const redlock = new Redlock(clients);
async function doWork() {
const resource = 'my-resource';
const ttl = 10000; // 10 seconds
let lockId: string | null = null;
try {
// Acquire the lock
lockId = await redlock.acquire(resource, ttl);
console.log('Lock acquired:', lockId);
// Perform your critical section code here
// Optionally extend the lock if needed
await redlock.extend(resource, lockId, ttl);
console.log('Lock extended');
// Release the lock
await redlock.release(resource, lockId);
console.log('Lock released');
} catch (error) {
console.error('Error:', error);
} finally {
// Ensure the lock is released if it was acquired
if (lockId) {
try {
await redlock.release(resource, lockId);
} catch (releaseError) {
console.error('Error releasing lock in finally block:', releaseError);
}
}
// Close Redis clients
redisClients.forEach((client) => client.disconnect());
}
}
doWork();Event Handling
The Redlock class extends EventEmitter and emits events during its operation. You can attach listeners to these events for logging or custom handling.
redlock.on('error', (error) => {
console.error('Redlock error:', error);
});
redlock.on('lockAcquired', ({ resource, lockId }) => {
console.log(`Lock acquired on resource ${resource} with ID ${lockId}`);
});
redlock.on('lockReleased', ({ resource, lockId }) => {
console.log(`Lock released on resource ${resource} with ID ${lockId}`);
});
redlock.on('lockExtended', ({ resource, lockId, ttl }) => {
console.log(`Lock on resource ${resource} extended with ID ${lockId} for ${ttl}ms`);
});API Documentation
Class: Redlock
The Redlock class provides methods to acquire, release, and extend locks on resources using the Redlock algorithm.
Constructor
constructor(clients: RedisClient[], options?: RedlockOptions)- Parameters:
clients(RedisClient[]): An array of Redis clients implementing theRedisClientinterface.options(RedlockOptions, optional):retryCount(number, optional): Number of times to retry acquiring the lock (default:3).retryDelay(number, optional): Delay between retries in milliseconds (default:200).
Methods
acquire(resource, ttl)
Acquires a distributed lock on the specified resource.
async acquire(resource: string, ttl: number): Promise<string>- Parameters:
resource(string): The resource key to lock.ttl(number): The time-to-live of the lock in milliseconds.
- Returns:
Promise<string>: The unique lock ID if the lock is acquired.
- Throws:
Errorif the lock cannot be acquired after the maximum retries.
release(resource, lockId)
Releases the distributed lock on the specified resource.
async release(resource: string, lockId: string): Promise<void>- Parameters:
resource(string): The resource key to unlock.lockId(string): The unique lock ID returned by acquire.
Throws:Errorif the lock cannot be released.
extend(resource, lockId, ttl)
Extends the duration of an existing lock.
async extend(resource: string, lockId: string, ttl: number): Promise<void>- Parameters:
resource(string): The resource key whose lock duration is to be extended.lockId(string): The unique lock ID returned by acquire.ttl(number): The new time-to-live for the lock in milliseconds.
Throws:Errorif the lock cannot be extended.
Events
The Redlock class emits the following events:
error: Emitted when an error occurs.
- Listener Parameters: (error: Error)
lockAcquired: Emitted when a lock is successfully acquired.
- Listener Parameters: ({ resource: string, lockId: string, validityTime: number })
lockReleased: Emitted when a lock is successfully released.
- Listener Parameters: ({ resource: string, lockId: string })
lockExtended: Emitted when a lock is successfully extended.
- Listener Parameters: ({ resource: string, lockId: string, ttl: number })
lockError: Emitted when an error occurs during lock acquisition on a client.
- Listener Parameters: ({ client: RedisClient, error: Error })
unlockError: Emitted when an error occurs during lock release on a client.
- Listener Parameters: ({ client: RedisClient, error: Error })
extendError: Emitted when an error occurs during lock extension on a client.
- Listener Parameters: ({ client: RedisClient, error: Error })
attemptFailed: Emitted when a lock acquisition attempt fails but retries are remaining.
- Listener Parameters: ({ resource: string, lockId: string, attempts: number })
Implementing Redis Clients
To use the Redlock class, you need to provide Redis clients that implement the RedisClient interface.
export interface RedisClient {
set(
key: string,
value: string,
options?: {
nx?: boolean;
px?: number; // Expiration in milliseconds
}
): Promise<'OK' | null>;
eval(
script: string,
keys: string[],
args: (string | number)[]
): Promise<number>;
disconnect(): void;
}Using ioredis
First, install ioredis as a dependency:
npm install ioredisThen, use the IORedisAdapter provided by the package:
import { Redlock, IORedisAdapter } from '@justin-kucerak/redlock';
import Redis from 'ioredis';
// Create Redis clients
const redisClients = [
new Redis({ host: 'localhost', port: 6379 }),
// ... other clients
];
// Wrap Redis clients with IORedisAdapter
const clients = redisClients.map((client) => new IORedisAdapter(client));
// Create Redlock instance
const redlock = new Redlock(clients);Using node-redis
First, install redis as a dependency:
npm install redisThen, use the NodeRedisAdapter provided by the package:
import { Redlock, NodeRedisAdapter } from '@justin-kucerak/redlock';
import { createClient } from 'redis';
// Create Redis clients
const redisClients = [
createClient({ url: 'redis://localhost:6379' }),
// ... other clients
];
// Ensure clients are connected
await Promise.all(redisClients.map((client) => client.connect()));
// Wrap Redis clients with NodeRedisAdapter
const clients = redisClients.map((client) => new NodeRedisAdapter(client));
// Create Redlock instance
const redlock = new Redlock(clients);Testing
The package includes unit tests to ensure correct functionality.
Unit Tests
Unit tests are written using Jest and can be run with:
npm run testThese tests use mock implementations of the RedisClient interface to simulate Redis behavior.
Contributing
Contributions are welcome! Please follow these guidelines:
- Fork the repository and create a new branch for your feature or bug fix.
- Write tests to cover your changes.
- Submit a pull request with a detailed description of your changes.
For major changes, please open an issue first to discuss what you would like to change.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Inspired by the Redlock algorithm as described by Redis.
- Thanks to the contributors of ioredis and node-redis for their excellent Redis client libraries.
- Hat tip to all developers who have contributed to similar distributed locking mechanisms.
11 months ago