UNPKG

@jbagatta/johnny-locke

Version:

A robust, strongly-consistent distributed locking library that provides atomic operations across multiple processes

127 lines 5.24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisDistributedLock = void 0; const util_1 = require("../util"); const lock_listener_1 = require("./lock-listener"); const data_model_1 = require("./data-model"); class RedisDistributedLock { constructor(redis, config) { this.redis = redis; this.config = config; this.active = false; this.lockListener = new lock_listener_1.LockListener(redis, config.namespace); this.active = true; } static async create(redis, config) { (0, util_1.validateLockConfiguration)(config); return new RedisDistributedLock(redis, config); } async withLock(key, timeoutMs, callback, lockDuration) { const lock = await this.acquireLock(key, timeoutMs, lockDuration); try { const updatedState = await callback(lock.value); const result = lock.update(updatedState); const updated = await this.releaseLock(key, result); if (!updated) { throw new util_1.TimeoutError(key); } return result; } catch (error) { await this.releaseLock(key, lock); throw error; } } async acquireLock(key, timeoutMs, lockDuration) { this.checkActive(); const namespacedKey = this.toNamespacedKey(key); const lockId = crypto.randomUUID(); const duration = (0, util_1.computeLockDuration)(this.config.defaultLockDurationMs, lockDuration); let now = Date.now(); const deadline = now + timeoutMs; while (now < deadline) { try { const unlockTimeout = deadline - now; // initialize listener before trying to acquire lock to avoid race condition const listener = this.lockListener.waitUntilNotified(namespacedKey, unlockTimeout); const lock = await this.getOrCreateLock(namespacedKey, lockId, duration); if (lock.lockId === lockId && lock.lockStatus === 'locked') { this.lockListener.cancel(listener); return new util_1.WritableObject(lock.lockObj, lockId); } // wait until notified of lock release to retry await listener; } catch { // suppress timeouts and retry } now = Date.now(); } const lastTry = await this.tryAcquireLock(key); if (lastTry.acquired) { return lastTry.value; } throw new util_1.TimeoutError(namespacedKey); } async tryAcquireLock(key, lockDuration) { this.checkActive(); const namespacedKey = this.toNamespacedKey(key); const lockId = crypto.randomUUID(); const duration = (0, util_1.computeLockDuration)(this.config.defaultLockDurationMs, lockDuration); const lock = await this.getOrCreateLock(namespacedKey, lockId, duration); if (lock.lockId === lockId && lock.lockStatus === 'locked') { return { acquired: true, value: new util_1.WritableObject(lock.lockObj, lockId) }; } return { acquired: false, value: undefined }; } async getOrCreateLock(namespacedKey, lockId, timeout) { const result = await this.redis.eval(data_model_1.tryAcquireLockLuaScript, 1, namespacedKey, lockId, timeout); return { lockId: result[0], lockStatus: result[1], lockObj: result[2] ? JSON.parse(result[2]) : null }; } async releaseLock(key, lockObj) { this.checkActive(); const namespacedKey = this.toNamespacedKey(key); const obj = JSON.stringify(lockObj.value); const result = await this.redis.eval(data_model_1.tryWriteLockLuaScript, 1, namespacedKey, lockObj.lockId, obj, this.config.objectExpiryMs ?? -1); const success = result === 1; if (success) { await this.lockListener.notify(namespacedKey, lockObj.value); } return success; } async wait(key, timeoutMs) { this.checkActive(); const namespacedKey = this.toNamespacedKey(key); const listener = this.lockListener.waitUntilNotified(namespacedKey, timeoutMs); const result = await this.redis.eval(data_model_1.getLockObjLuaScript, 1, namespacedKey); if (result[0] === 'locked') { const value = await listener; return { value }; } this.lockListener.cancel(listener); return { value: result[1] ? JSON.parse(result[1]) : null }; } async delete(key) { this.checkActive(); const lock = await this.acquireLock(key, this.config.defaultLockDurationMs); return await this.releaseLock(key, lock.update(null)); } close() { this.lockListener.close(); this.active = false; } toNamespacedKey(key) { return `${this.config.namespace}.${key}`; } checkActive() { if (!this.active) { throw new Error('RedisDistributedLock closed'); } } } exports.RedisDistributedLock = RedisDistributedLock; //# sourceMappingURL=redis-distributed-lock.js.map