UNPKG

@apiratorjs/locking-redis

Version:

An extension to the core @apiratorjs/locking library, providing Redis-based implementations of distributed mutexes and semaphores for true cross-process concurrency control in Node.js.

212 lines (152 loc) 6.83 kB
# @apiratorjs/locking-redis [![NPM version](https://img.shields.io/npm/v/@apiratorjs/locking-redis.svg)](https://www.npmjs.com/package/@apiratorjs/locking-redis) [![License: MIT](https://img.shields.io/npm/l/@apiratorjs/locking-redis.svg)](https://github.com/apiratorjs/locking-redis/blob/main/LICENSE) An extension to the core [@apiratorjs/locking](https://github.com/apiratorjs/locking) library, providing a Redis-backed `IDistributedLockManager` with distributed mutexes and semaphores for true cross-process concurrency control in Node.js. > **Note:** Requires Node.js version **>=16.4.0**, [@apiratorjs/locking](https://github.com/apiratorjs/locking) **^5.0.0**, > and a running Redis instance (version 5+ recommended). > > Upgrading from 1.x? See [CHANGELOG](./CHANGELOG.md) and [2.0.0 release notes](./RELEASE_NOTES.md). --- ## Why Use Redis for Distributed Locking? - **Multi-instance deployments**: If you have multiple Node.js processes or servers behind a load balancer, an in-memory lock is insufficient. Redis provides a single, centralized coordination point. - **Fault tolerance**: Configurable timeouts (TTLs) prevent indefinite locks if a process crashes. - **Scalability**: Redis can handle many simultaneous locking requests at scale. --- ## Features - **`RedisDistributedLockManager`** — implements `types.IDistributedLockManager` from `@apiratorjs/locking`. - **Distributed Mutex and Semaphore** — same acquire / release / cancel / wait-for-unlock API as the core distributed primitives, coordinated through Redis. - **Named locks** — the same name returns the same live instance while it is alive; `list()`, `count()`, `snapshot()`, `cancelAll()`, and `destroyAll()` for inspection and shutdown. - **Time-limited locks (TTL)** — prevents deadlocks if processes crash without releasing. - **Cancellation, timeouts, and FIFO waiters** — cancel blocked acquisitions, fail fast with `timeoutMs: 0`, queue waiters in order. - **Read-write locks** — not implemented yet; `readWriteLock()` throws `LockingError`. --- ## Installation Install with npm: ```bash npm install @apiratorjs/locking @apiratorjs/locking-redis ``` Or with yarn: ```bash yarn add @apiratorjs/locking @apiratorjs/locking-redis ``` --- ## Usage > Default acquire timeout is 1 minute (same as the core library). Pass `timeoutMs: 0` to fail fast with a > `TimeoutLockingError` when the lock is not immediately available. > > `cancel()` / `cancelAll()` reject pending acquisitions only: held locks stay held, and > `waitForUnlock()` / `waitForAnyUnlock()` / `waitForFullyUnlock()` keep waiting until holders release (or until > `destroy()` / `destroyAll()`, which resolves those waiters). ### Quick start ```typescript import { RedisDistributedLockManager } from "@apiratorjs/locking-redis"; const locks = await RedisDistributedLockManager.create({ url: "redis://localhost:6379", }); async function example() { const mutex = locks.mutex("shared-resource"); const releaser = await mutex.acquire({ timeoutMs: 5000 }); try { console.log("Distributed mutex acquired"); } finally { await releaser.release(); } await locks.semaphore("api-rate-limiter", 5).runExclusive(async () => { console.log("Distributed semaphore slot acquired"); }); } process.on("SIGTERM", async () => { await locks.destroyAll("Shutting down"); await locks.disconnect(); }); ``` ### Inject an existing Redis client When you already own a `redis` client, pass it into the constructor. In that case `disconnect()` is a no-op — you close the client yourself. ```typescript import { createClient } from "redis"; import { RedisDistributedLockManager } from "@apiratorjs/locking-redis"; const redisClient = createClient({ url: "redis://localhost:6379" }); await redisClient.connect(); const locks = new RedisDistributedLockManager({ redisClient }); ``` ### Distributed Mutex ```typescript const mutex = locks.mutex("orders"); const releaser = await mutex.acquire({ timeoutMs: 5000 }); try { // Critical section — exclusive across all processes sharing this Redis } finally { await releaser.release(); } await mutex.runExclusive(async () => { // Acquired and released automatically }); await mutex.cancel("Operation cancelled"); await mutex.waitForUnlock(); ``` ### Distributed Semaphore ```typescript const semaphore = locks.semaphore("uploads", 5); const releaser = await semaphore.acquire({ timeoutMs: 5000 }); try { // Up to 5 concurrent holders across processes } finally { await releaser.release(); } await semaphore.runExclusive(async () => { // Acquired and released automatically }); await semaphore.cancelAll("Operation cancelled"); await semaphore.waitForAnyUnlock(); await semaphore.waitForFullyUnlock(); ``` ### Managing locks ```typescript import { ELockDisplayType } from "@apiratorjs/locking"; locks.hasMutex("orders"); locks.hasSemaphore("uploads"); locks.count(); locks.count(ELockDisplayType.Semaphore); locks.list(); await locks.snapshot(); await locks.cancelAll("Draining before deploy"); await locks.destroyAll("Shutting down"); ``` Requesting the same semaphore name with a different `maxCount` throws `LockConfigMismatchError`. ### Swapping backends Because both managers implement `IDistributedLockManager`, application code can depend on the interface and receive either an in-memory or Redis manager: ```typescript import { types, InMemoryDistributedLockManager } from "@apiratorjs/locking"; import { RedisDistributedLockManager } from "@apiratorjs/locking-redis"; export const locks: types.IDistributedLockManager = process.env.REDIS_URL ? await RedisDistributedLockManager.create({ url: process.env.REDIS_URL }) : new InMemoryDistributedLockManager(); ``` Nothing is global, so a Redis-backed manager and an in-memory one can coexist — useful when only part of the system needs cross-process coordination, and in tests. ### Low-level classes `RedisDistributedMutex` and `RedisDistributedSemaphore` are also exported for advanced use (for example custom managers). Prefer `RedisDistributedLockManager` in application code so named instances, listing, and shutdown stay consistent. --- ## Error handling Errors come from `@apiratorjs/locking`: | Error Class | When thrown | |-------------|-------------| | `TimeoutLockingError` | `acquire()` exceeds `timeoutMs` | | `CancelledLockingError` | `cancel()` / `cancelAll()` or destroy | | `LockNotFoundError` | Lock was destroyed | | `LockConfigMismatchError` | Same name requested with conflicting `maxCount` | | `LockingError` | Base class; also thrown by unimplemented `readWriteLock()` | --- ## Contributing Contributions, issues, and feature requests are welcome! Please open an issue or submit a pull request on [GitHub](https://github.com/apiratorjs/locking-redis).