@jbagatta/johnny-locke
Version:
A robust, strongly-consistent distributed locking library that provides atomic operations across multiple processes
127 lines • 5.24 kB
JavaScript
"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