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