1.0.0 • Published 11 months ago

@justin-kucerak/redlock v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

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

  • 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 RedisClient interface.
  • 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/redlock

You'll also need to install a Redis client library. The package supports ioredis and node-redis via provided adapters.

For ioredis:

npm install ioredis

For node-redis:

npm install redis

Note: 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 the RedisClient interface.
    • 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:
    • Error if 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:
    • Error if 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:
    • Error if 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 ioredis

Then, 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 redis

Then, 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 test

These tests use mock implementations of the RedisClient interface to simulate Redis behavior.

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository and create a new branch for your feature or bug fix.
  2. Write tests to cover your changes.
  3. 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.