node-redlock

repository·main·Indexed 24 days ago

https://github.com/mike-marcacci/node-redlock

A Node.js implementation of the Redlock algorithm for distributed Redis locks. It enables multiple clients to coordinate access to shared resources using a quorum of Redis nodes to ensure high availability and mutual exclusion. Features include a high-level `using` API for auto-extending locks, manual acquire/extend/release lifecycles, and support for Redis Clusters via hash tags. Version v5.0.0-beta.2 provides both ESM and CommonJS support.

Tokens
3.3K
Snippets
7
Records
18
Agent score
84%

What's inside node-redlock

  1. Locking multiple resources in a Redis Cluster

    main

    When using a Redis Cluster, all keys in a single command must belong to the same node. If you need to lock multiple resources together using redlock, you MUST use Redis hash tags (e.g., {tag}key) to ensure all resource strings resolve to the same node.

    Strategies:

    • Generic Prefix: Use a single generic prefix like {redlock}my_resource to ensure all lock keys resolve to the same node.
    • Attribute-based Tagging: Use a common attribute (like a tenant ID) as a hash tag, e.g., {tenant123}resource_name, to balance distribution while allowing multi-resource locks for that specific tenant.
  2. How to check if a resource is locked

    main

    Redlock is designed for exclusivity guarantees, not for reporting ownership status. It cannot tell you if a lock exists on the other side of a network partition.

    If you need to check if a resource is currently unavailable (effectively "locked"), you can attempt to acquire a lock with retryCount: 0. If it fails, treat the resource as unavailable.

    Retry Settings:

    • retryCount: 0: Attempt once; fail immediately if locked.
    • retryCount: -1: Unlimited retries until the lock is acquired.
  3. Import Redlock in CommonJS projects

    main

    In version 5, redlock is primarily an ESM module. For CommonJS projects, a transpiled version is provided. Ensure you use either the ESM or CommonJS version, but not both.

    To import the Redlock class in CommonJS:

    const { default: Redlock } = require("redlock");

    Note: Version 6 will stop distributing the CommonJS version.

  4. Configure the Redlock instance

    main

    Instantiate Redlock by passing an array of at least one ioredis client and an optional options object.

    Important: Do not change the properties of the Redlock object after it has been used, as this can cause unintended consequences for live locks.

    Configuration Options

    • driftFactor: The expected clock drift (multiplied by lock TTL to determine drift time). Default is typically 0.01.
    • retryCount: The maximum number of times Redlock will attempt to lock a resource before erroring.
    • retryDelay: The time in milliseconds between attempts.
    • retryJitter: The maximum time in milliseconds randomly added to retries to improve performance under high contention.
    • automaticExtensionThreshold: The minimum remaining time on a lock (in ms) before an extension is automatically attempted when using the using API.
    import Client from "ioredis";
    import Redlock from "redlock";
    
    const redisA = new Client({ host: "a.redis.example.com" });
    const redisB = new Client({ host: "b.redis.example.com" });
    const redisC = new Client({ host: "c.redis.example.com" });
    
    const redlock = new Redlock(
      [redisA, redisB, redisC],
      {
        driftFactor: 0.01,
        retryCount: 10,
        retryDelay: 200,
        retryJitter: 200,
        automaticExtensionThreshold: 500,
      }
    );
  5. Initialize Redlock with Redis clients

    main

    To use Redlock, instantiate the Redlock class by providing an iterable of Redis clients (e.g., ioredis Redis or Cluster instances). You can optionally provide custom Settings or custom Lua scripts for acquiring, extending, or releasing locks.

    Note: Properties of the Redlock instance should not be modified after instantiation to avoid side effects on live locks.

  6. Handle Redlock errors and monitoring

    main

    Redlock is designed for high availability and ignores errors from a minority of Redis instances. However, you can monitor these background errors by listening to the error event on the Redlock instance.

    To avoid logging expected lock contention, check if the error is an instance of ResourceLockedError.

    Additionally, Lock and ExecutionError classes provide an attempt property containing per-attempt and per-client statistics, including errors.

    redlock.on("error", (error) => {
      // Ignore cases where a resource is explicitly marked as locked on a client.
      if (error instanceof ResourceLockedError) {
        return;
      }
    
      // Log all other errors.
      console.error(error);
    });
  7. Acquire and release locks manually

    main

    You can manage the lock lifecycle manually using acquire, extend, and release.

    1. Acquire: Use redlock.acquire(resources, ttl) to get a Lock instance.
    2. Extend: Use lock.extend(ttl) to extend the duration. This returns a new Lock instance; you must update your reference to the lock.
    3. Release: Use lock.release() in a finally block to ensure the lock is freed even if the routine fails.
    // Acquire a lock.
    let lock = await redlock.acquire(["a"], 5000);
    try {
      // Do something...
      await something();
    
      // Extend the lock. Note that this returns a new `Lock` instance.
      lock = await lock.extend(5000);
    
      // Do something else...
      await somethingElse();
    } finally {
      // Release the lock.
      await lock.release();
    }
  8. Use the `using` API for auto-extending locks

    main

    The using method executes a routine within the context of an auto-extending lock. It returns a promise that resolves with the routine's value.

    If an automatic lock extension fails, the provided AbortSignal will be updated. You should check signal.aborted within your routine to ensure the lock is still valid. If it is aborted, you should throw signal.error to stop your operation.

    Parameters:

    1. resources: An array of strings representing the resources to lock.
    2. ttl: The requested lock duration in milliseconds (must be an integer).
    3. callback: An async function receiving an AbortSignal.

    Note: If the routine is aborted due to extension failure, the error thrown out of redlock.using will be "The operation was unable to achieve a quorum during its retry window." rather than the specific signal.error.

    await redlock.using(["foo", "bar"], 5000, async (signal) => {
      // Do something...
      await something("foo");
    
      // Make sure any attempted lock extension has not failed.
      if (signal.aborted) {
        throw signal.error;
      }
    
      // Do something else...
      await somethingElse("bar");
    });
  9. Configure Redis topologies via Docker Compose

    main

    The docker-compose.yml file provides several predefined Redis topologies to test or simulate different high-availability scenarios for node-redlock. You can use these services to set up your environment:

    • Single standalone instance: A single redis:6 container (redis-single-instance). Not highly available.
    • Multiple standalone instances: Multiple independent redis:6 containers (redis-multi-instance-a, b, c). Provides high availability.
    • Single cluster: A Redis cluster setup using 6 nodes with 1 replica (redis-single-cluster).
    • Multi cluster: Multiple independent Redis clusters (redis-multi-cluster-a, b, c) for testing extreme high-availability or complex distributed scenarios.
  10. Configure Redlock settings

    main

    You can customize the behavior of the Redlock instance via the Settings object during construction.

    KeyTypeDefaultDescription
    driftFactornumber0.01The fraction of the duration used to account for clock drift.
    retryCountnumber10Number of times to retry an operation before failing. Use -1 for infinite retries.
    retryDelaynumber200Base delay between retries in ms.
    retryJitternumber100Random jitter applied to the retry delay to prevent thundering herds.
    automaticExtensionThresholdnumber500How many ms before expiration the using() method attempts to extend the lock.
  11. Handle Redlock errors

    main

    When working with Redlock, you should handle the following error types:

    • ResourceLockedError: Thrown when you attempt to acquire a lock that is already held by another process.
    • ExecutionError: Thrown when a distributed operation (acquire, extend, or release) fails to reach a quorum of Redis nodes. This error includes an attempts property containing ExecutionStats for debugging.

    Additionally, the Redlock instance is an EventEmitter. It emits an 'error' event whenever it encounters a Redis error. While Redlock is designed to be resilient to minority failures, you should listen to this event for observability.