HazelJS Distributed Lock Package

npm downloads

@hazeljs/distributed-lock provides a robust distributed locking system for HazelJS applications. It allows you to synchronize access to shared resources across multiple service instances, preventing race conditions and ensuring mutual exclusion in distributed environments.

Quick Reference

  • Purpose: Synchronize access to critical resources across multiple application instances to ensure atomic execution.
  • When to use: Use when you have multiple instances of your service and need to ensure only one instance processes a specific resource (e.g., a payment, an inventory update, or a singleton background task) at a time.
  • Key concepts: @DistributedLock decorator, LockManager, RedisBackend, MemoryBackend, TTL (Time-to-Live), Wait & Retry strategies, Dynamic Key Resolution.
  • Inputs: Lock key (string with templates), TTL (ms), retry options.
  • Outputs: An acquired lock object (ILock) or null if acquisition fails.
  • Dependencies: @hazeljs/core, redis (optional for production).
  • Common patterns: Decorate method with @DistributedLock({ key: '...' }) → key resolves dynamically from arguments → lock acquired before method runs → method executes → lock released automatically.
  • Common mistakes: Forgetting to set a reasonable TTL (leads to deadlocks on crash); using the In-Memory backend in a multi-node production cluster; using keys that are too broad (lowers concurrency).

Architecture Mental Model

graph TD
  A["Request 1 (Node A)"] --> B["LockManager"]
  C["Request 2 (Node B)"] --> B
  B --> D{"Backend Layer"}
  D --> E["Redis (Distributed Store)"]
  D --> F["In-Memory (Local Dev)"]
  E --> G["Resource Access Granted"]
  E --> H["Resource Access Denied / Wait"]
  
  style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style C fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style B fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style E fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff

When to Use @hazeljs/distributed-lock

ScenarioUseBackend
Preventing double-payments@DistributedLockRedis
Synchronizing a global singleton jobLockManager.acquire()Redis
Protecting local file access@DistributedLockIn-Memory
Atomic inventory updates@DistributedLockRedis

Installation

npm install @hazeljs/distributed-lock

For production environments using Redis:

npm install redis

HazelJS DistributedLockModule Registration

Register the DistributedLockModule in your application's root module to configure the default backend and global settings.

src/app.module.ts
import { HazelModule } from '@hazeljs/core';
import { DistributedLockModule } from '@hazeljs/distributed-lock';

@HazelModule({
  imports: [
    DistributedLockModule.forRoot({
      backend: 'redis',
      prefix: 'hazel:lock:',
      defaultTtl: 30000,
      redis: {
        host: process.env.REDIS_HOST || 'localhost',
        port: 6379,
      },
    }),
  ],
})
export class AppModule {}

@DistributedLock Decorator

The @DistributedLock decorator provides a declarative way to protect method execution with mutual exclusion.

Usage

import { Injectable } from '@hazeljs/core';
import { DistributedLock } from '@hazeljs/distributed-lock';

@Injectable()
export class InventoryService {
  @DistributedLock({ 
    key: 'stock-update-{{productId}}', 
    ttl: 10000,
    wait: true 
  })
  async updateStock(productId: string, quantity: number) {
    // This method is now safe from concurrent execution across all nodes
    // for the same productId.
    return await this.db.stocks.decrement(productId, quantity);
  }
}

Dynamic Key Resolution

The key property supports template strings that automatically resolve from method arguments:

  • {{productId}} - Resolves from the productId argument.
  • {{body.id}} - Resolves from an object argument named body.
  • {{user.sub}} - Resolves from an object argument named user.

LockManager Service

For programmatic lock control, inject the LockManager service.

import { Service } from '@hazeljs/core';
import { LockManager } from '@hazeljs/distributed-lock';

@Service()
export class CriticalTaskService {
  constructor(private readonly lockManager: LockManager) {}

  async processTask(taskId: string) {
    const lock = await this.lockManager.acquire(`task:${taskId}`, {
      ttl: 5000,
      wait: true,
      waitTimeout: 2000
    });

    if (!lock) {
      throw new Error('Could not acquire lock');
    }

    try {
      await this.runTask(taskId);
    } finally {
      await this.lockManager.release(lock);
    }
  }
}

Configuration Options

OptionTypeDescriptionDefault
backendstring'memory' or 'redis'.'memory'
prefixstringGlobal prefix for all keys.'hazel:lock:'
defaultTtlnumberDefault expiration in ms.30000
waitbooleanWhether to wait for a lock.false
retryCountnumberRetries if wait is true.3
retryDelaynumberDelay between retries (ms).100