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.

146 lines 5.53 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisDistributedMutex = void 0; const node_assert_1 = __importDefault(require("node:assert")); const node_crypto_1 = __importDefault(require("node:crypto")); const locking_1 = require("@apiratorjs/locking"); const constants_1 = require("./constants"); const distributed_releaser_1 = require("./distributed-releaser"); const base_distributed_lock_primitive_1 = require("./base-distributed-lock-primitive"); const redis_script_1 = require("./redis-script"); /** * Only release if the lock key's value matches our lock token. */ const RELEASE_SCRIPT = new redis_script_1.RedisScript(` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) end return 0 `); class RedisDistributedMutex extends base_distributed_lock_primitive_1.BaseDistributedLockPrimitive { constructor(props) { node_assert_1.default.ok(props.name, "RedisDistributedMutex requires a non-empty name."); super({ ...props, name: `${locking_1.ELockDisplayType.Mutex}:${props.name}` }); } async destroy(message) { if (this.destroyed) { return; } this.destroyed = true; await this.redisClient.del(this.name); if (this.redisSubscriber) { await this.redisSubscriber.unsubscribe(`${this.name}:cancel`); await this.redisSubscriber.unsubscribe(`${this.name}:release`); await this.redisSubscriber.unsubscribe(`${this.name}:destroy`); await this.redisSubscriber.disconnect(); this.redisSubscriber = undefined; } await this.redisClient.publish(`${this.name}:destroy`, "destroyed"); this.rejectQueuedAcquirers(new locking_1.CancelledLockingError(message ?? "Mutex destroyed")); this.resolveUnlockWaiters(); } async acquire(params) { this.ensureAlive(); await this.ensureSubscriber(); // `??` and not `||`: timeoutMs 0 means "fail fast", not "use the default". const timeoutMs = params?.timeoutMs ?? constants_1.DEFAULT_TTL_MS; // Redis PX must be positive; a zero wait timeout still needs a real lock TTL. const lockTtlMs = timeoutMs > 0 ? timeoutMs : constants_1.DEFAULT_TTL_MS; const acquireToken = await this.tryAcquire(lockTtlMs); if (acquireToken) { return new distributed_releaser_1.DistributedReleaser(() => this.release(acquireToken), acquireToken); } if (timeoutMs === 0) { throw new locking_1.TimeoutLockingError("Timeout acquiring"); } return new Promise((resolve, reject) => { const deferred = { resolve, reject, ttlMs: lockTtlMs, timer: null, }; deferred.timer = setTimeout(() => { const index = this.queue.indexOf(deferred); if (index !== -1) { this.queue.splice(index, 1); } reject(new locking_1.TimeoutLockingError("Timeout acquiring")); }, timeoutMs); deferred.timer.unref(); this.queue.push(deferred); }); } async cancel(errMessage) { this.ensureAlive(); const msg = `cancel:${errMessage ?? "Mutex cancelled"}`; await this.redisClient.publish(`${this.name}:cancel`, msg); } async isLocked() { this.ensureAlive(); const val = await this.redisClient.get(this.name); return val !== null; } async runExclusive(...args) { let callback; let params; if (args.length === 1) { callback = args[0]; } else { params = args[0]; callback = args[1]; } const releaser = await this.acquire(params); try { return await callback(); } finally { await releaser.release(); } } async waitForUnlock() { this.ensureAlive(); return this.waitForUnlockEvent(async () => { // Treat destroy as unlocked so in-flight `:release` notify checks do not // throw LockNotFoundError via isLocked() after destroyed is set. if (this.destroyed) { return true; } return !(await this.isLocked()); }); } async tryAcquire(timeoutMs) { const token = `${this.name}:${node_crypto_1.default.randomUUID()}`; const result = await this.redisClient.set(this.name, token, { NX: true, PX: timeoutMs, }); if (result === "OK") { return token; } return undefined; } async release(token) { if (this.destroyed) { return; } const result = await RELEASE_SCRIPT.run(this.redisClient, { keys: [this.name], arguments: [token], }); if (result === 1) { await this.redisClient.publish(`${this.name}:release`, token); } } ensureAlive() { if (this.destroyed) { throw new locking_1.LockNotFoundError(`${locking_1.ELockDisplayType.Mutex} '${this.name}' does not exist`); } } } exports.RedisDistributedMutex = RedisDistributedMutex; //# sourceMappingURL=redis-distributed-mutex.js.map