UNPKG

node-redisson

Version:

Distributed lock with Redis implementation for Node.js

140 lines (139 loc) 5.16 kB
"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;