@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
JavaScript
"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