node-redisson
Version:
Distributed lock with Redis implementation for Node.js
140 lines (139 loc) • 5.16 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpirationEntry = exports.RedissonBaseLock = void 0;
const node_crypto_1 = require("node:crypto");
const RedissonLockError_1 = require("../errors/RedissonLockError");
class RedissonBaseLock {
static prefixName(prefix, name) {
const _name = name.startsWith('{') ? name : `{${name}}`;
return `${prefix}:${_name}`;
}
constructor(commandExecutor, lockName, clientId) {
this.commandExecutor = commandExecutor;
this.id = commandExecutor.id;
this.lockName = lockName;
this._clientId = clientId ?? (0, node_crypto_1.randomUUID)();
this.entryName = `${this.id}:${lockName}`;
this.internalLockLeaseTime = commandExecutor.redissonConfig.lockWatchdogTimeout;
}
get name() {
return this.lockName;
}
get clientId() {
return this._clientId;
}
async unlock() {
const requestId = (0, node_crypto_1.randomUUID)();
const unlockResult = await this.unlockInner(this.clientId, requestId, 10);
await this.commandExecutor.redis.del(this.getUnlockLatchName(requestId));
if (unlockResult === null) {
throw new RedissonLockError_1.RedissonLockError(`attempt to unlock lock, not locked by current client`, this.lockName, this.id, this.clientId);
}
this.cancelExpirationRenewal(unlockResult, this.clientId);
}
async isLocked() {
const count = await this.commandExecutor.redis.exists(this.lockName);
return count > 0;
}
getClientName(clientId) {
return `${this.id}:${clientId}`;
}
getUnlockLatchName(requestId) {
return `${RedissonBaseLock.prefixName('redisson_unlock_latch', this.lockName)}:${requestId}`;
}
scheduleExpirationRenewal(clientId) {
const oldEntry = RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(this.entryName);
if (oldEntry) {
oldEntry.addClientId(clientId);
}
else {
const entry = new ExpirationEntry();
entry.addClientId(clientId);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.set(this.entryName, entry);
this.renewExpiration();
}
}
renewExpiration() {
const ee = RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(this.entryName);
if (!ee) {
return;
}
ee.timeoutId = setTimeout(async () => {
try {
const ent = RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(this.entryName);
if (!ent) {
return;
}
const clientId = ent.firstClientId;
if (!clientId) {
return;
}
const result = await this.commandExecutor.redis.rRenewExpiration(this.lockName, this.internalLockLeaseTime, this.getClientName(clientId));
if (result) {
this.renewExpiration();
}
else {
this.cancelExpirationRenewal(true);
}
}
catch (e) {
// TODO std logger
console.error(e);
}
}, Number(`${this.internalLockLeaseTime / 3n}`));
}
cancelExpirationRenewal(unlockResult, clientId) {
// Recover the lockLeaseTime from config
if (unlockResult) {
this.internalLockLeaseTime = this.commandExecutor.redissonConfig.lockWatchdogTimeout;
}
// Remove entry from map
const task = RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(this.entryName);
if (!task) {
return;
}
if (clientId) {
task.removeClientId(clientId);
}
if (!clientId || task.hasNoClients) {
if (task.timeoutId) {
clearTimeout(task.timeoutId);
}
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.delete(this.entryName);
}
}
}
exports.RedissonBaseLock = RedissonBaseLock;
RedissonBaseLock.EXPIRATION_RENEWAL_MAP = new Map();
class ExpirationEntry {
constructor() {
this.clientsQueue = [];
this.clientIds = new Map();
}
addClientId(clientId) {
this.clientIds.set(clientId, this.getClientCounter(clientId) + 1);
this.clientsQueue.push(clientId);
}
removeClientId(clientId) {
if (this.clientIds.has(clientId)) {
// decrement the counter
const counter = this.getClientCounter(clientId) - 1;
// if counter <= 0, remove the clientId
if (counter <= 0) {
this.clientsQueue = this.clientsQueue.filter((it) => it !== clientId);
this.clientIds.delete(clientId);
}
this.clientIds.set(clientId, counter);
}
}
get hasNoClients() {
return this.clientsQueue.length <= 0;
}
get firstClientId() {
return this.clientsQueue[0];
}
getClientCounter(clientId, defaultCounter = 0) {
return this.clientIds.get(clientId) ?? defaultCounter;
}
}
exports.ExpirationEntry = ExpirationEntry;