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.

232 lines 8.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisDistributedLockManager = void 0; const node_assert_1 = __importDefault(require("node:assert")); const redis_1 = require("redis"); const locking_1 = require("@apiratorjs/locking"); const redis_distributed_mutex_1 = require("./redis-distributed-mutex"); const redis_distributed_semaphore_1 = require("./redis-distributed-semaphore"); /** * Redis-backed implementation of IDistributedLockManager. * * Owns a set of named distributed locks keyed in Redis: hands out the same * instance for the same name while that lock is alive, and knows what it handed * out so an application can list, cancel, or tear everything down on shutdown. * * ```ts * const locks = await RedisDistributedLockManager.create({ url: "redis://localhost:6379" }); * await locks.mutex("orders").runExclusive(() => shipOrder()); * * process.on("SIGTERM", async () => { * await locks.destroyAll("Shutting down"); * await locks.disconnect(); * }); * ``` */ class RedisDistributedLockManager { constructor(props, options) { this.managedLocks = new Map(); node_assert_1.default.ok(props.redisClient, "RedisDistributedLockManager requires a redisClient."); this.redisClient = props.redisClient; this.ownsClient = options?.ownsClient ?? false; } /** * Connects to Redis and returns a manager that owns the client lifecycle. */ static async create(options) { const redisClient = (0, redis_1.createClient)({ url: options.url }); await redisClient.connect(); return new RedisDistributedLockManager({ redisClient }, { ownsClient: true }); } getRedisClient() { return this.redisClient; } /** * Disconnects the Redis client when this manager created it via {@link create}. * No-op when the client was injected from outside. */ async disconnect() { if (!this.ownsClient) { return; } if (this.redisClient.isOpen) { await this.redisClient.disconnect(); } } mutex(name) { node_assert_1.default.ok(name, "RedisDistributedLockManager requires a non-empty lock name."); const existing = this.takeAlive(locking_1.ELockDisplayType.Mutex, name); if (existing?.kind === locking_1.ELockDisplayType.Mutex) { return existing.lock; } const lock = new redis_distributed_mutex_1.RedisDistributedMutex({ name, redisClient: this.redisClient, }); this.managedLocks.set(this.keyOf(locking_1.ELockDisplayType.Mutex, name), { kind: locking_1.ELockDisplayType.Mutex, name, lock, }); return lock; } semaphore(name, maxCount) { node_assert_1.default.ok(name, "RedisDistributedLockManager requires a non-empty lock name."); node_assert_1.default.ok(maxCount > 0, "maxCount must be greater than 0"); const existing = this.takeAlive(locking_1.ELockDisplayType.Semaphore, name); if (existing?.kind === locking_1.ELockDisplayType.Semaphore) { if (existing.maxCount !== maxCount) { throw new locking_1.LockConfigMismatchError(`${locking_1.ELockDisplayType.Semaphore} '${name}' is already registered with maxCount ${existing.maxCount}, requested ${maxCount}`); } return existing.lock; } const lock = new redis_distributed_semaphore_1.RedisDistributedSemaphore({ name, maxCount, redisClient: this.redisClient, }); this.managedLocks.set(this.keyOf(locking_1.ELockDisplayType.Semaphore, name), { kind: locking_1.ELockDisplayType.Semaphore, name, lock, maxCount, }); return lock; } readWriteLock(name, maxReaders) { void name; void maxReaders; throw new locking_1.LockingError("RedisDistributedLockManager.readWriteLock is not implemented yet"); } hasMutex(name) { return this.takeAlive(locking_1.ELockDisplayType.Mutex, name) !== undefined; } hasSemaphore(name) { return this.takeAlive(locking_1.ELockDisplayType.Semaphore, name) !== undefined; } hasRWLock(name) { return this.takeAlive(locking_1.ELockDisplayType.RWLock, name) !== undefined; } list() { this.dropDestroyed(); return [...this.managedLocks.values()].map((managed) => this.describe(managed)); } count(kind) { this.dropDestroyed(); if (kind === undefined) { return this.managedLocks.size; } let total = 0; for (const managed of this.managedLocks.values()) { if (managed.kind === kind) { total++; } } return total; } async snapshot() { this.dropDestroyed(); return Promise.all([...this.managedLocks.values()].map(async (managed) => { const info = this.describe(managed); try { if (managed.kind === locking_1.ELockDisplayType.Mutex) { return { ...info, isLocked: await managed.lock.isLocked() }; } if (managed.kind === locking_1.ELockDisplayType.Semaphore) { return { ...info, isLocked: await managed.lock.isLocked(), freeCount: await managed.lock.freeCount(), }; } return { ...info, isWriteLocked: await managed.lock.isWriteLocked(), isReadLocked: await managed.lock.isReadLocked(), activeReaders: await managed.lock.activeReaders(), }; } catch (error) { if (error instanceof locking_1.LockNotFoundError) { return info; } throw error; } })); } async cancelAll(errMessage) { this.dropDestroyed(); const results = await Promise.allSettled([...this.managedLocks.values()].map((managed) => this.cancelOne(managed, errMessage))); this.throwIfAnyFailed(results, "Failed to cancel some locks"); } async destroyAll(errMessage) { const managedLocks = [...this.managedLocks.values()]; this.managedLocks.clear(); const results = await Promise.allSettled(managedLocks.map(async (managed) => { if (managed.lock.isDestroyed) { return; } if (errMessage !== undefined) { await this.cancelOne(managed, errMessage); } await managed.lock.destroy(); })); this.throwIfAnyFailed(results, "Failed to destroy some locks"); } cancelOne(managed, errMessage) { return managed.kind === locking_1.ELockDisplayType.Mutex ? managed.lock.cancel(errMessage) : managed.lock.cancelAll(errMessage); } describe(managed) { const info = { kind: managed.kind, name: managed.name, implementation: managed.lock.implementation, isDestroyed: managed.lock.isDestroyed, }; if (managed.kind === locking_1.ELockDisplayType.Semaphore) { info.maxCount = managed.maxCount; } if (managed.kind === locking_1.ELockDisplayType.RWLock && managed.maxReaders !== undefined) { info.maxReaders = managed.maxReaders; } return info; } keyOf(kind, name) { return `${kind}:${name}`; } takeAlive(kind, name) { const key = this.keyOf(kind, name); const managed = this.managedLocks.get(key); if (!managed) { return undefined; } if (managed.lock.isDestroyed) { this.managedLocks.delete(key); return undefined; } return managed; } dropDestroyed() { for (const [key, managed] of this.managedLocks) { if (managed.lock.isDestroyed) { this.managedLocks.delete(key); } } } throwIfAnyFailed(results, message) { const errors = results .filter((result) => result.status === "rejected") .map((result) => result.reason) .filter((reason) => !(reason instanceof locking_1.LockNotFoundError)); if (errors.length > 0) { throw new AggregateError(errors, message); } } } exports.RedisDistributedLockManager = RedisDistributedLockManager; //# sourceMappingURL=redis-distributed-lock-manager.js.map