HazelJS Distributed Lock Package
@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:
@DistributedLockdecorator,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) ornullif 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:#fffWhen to Use @hazeljs/distributed-lock
| Scenario | Use | Backend |
|---|---|---|
| Preventing double-payments | @DistributedLock | Redis |
| Synchronizing a global singleton job | LockManager.acquire() | Redis |
| Protecting local file access | @DistributedLock | In-Memory |
| Atomic inventory updates | @DistributedLock | Redis |
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 theproductIdargument.{{body.id}}- Resolves from an object argument namedbody.{{user.sub}}- Resolves from an object argument nameduser.
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
| Option | Type | Description | Default |
|---|---|---|---|
backend | string | 'memory' or 'redis'. | 'memory' |
prefix | string | Global prefix for all keys. | 'hazel:lock:' |
defaultTtl | number | Default expiration in ms. | 30000 |
wait | boolean | Whether to wait for a lock. | false |
retryCount | number | Retries if wait is true. | 3 |
retryDelay | number | Delay between retries (ms). | 100 |