@nivinjoseph/n-data
Version:
Data access library for Postgres based on Knex
169 lines (166 loc) • 7.54 kB
JavaScript
import { __esDecorate, __runInitializers, __setFunctionName } from "tslib";
import { Delay, Duration, Make, Uuid } from "@nivinjoseph/n-util";
import { given } from "@nivinjoseph/n-defensive";
import { ApplicationException, ObjectDisposedException } from "@nivinjoseph/n-exception";
import { inject } from "@nivinjoseph/n-ject";
import { createHash } from "node:crypto";
let RedisDistributedLockService = (() => {
let _classDecorators = [inject("RedisClient")];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
var RedisDistributedLockService = _classThis = class {
constructor(redisClient, config) {
this._defaultConfig = {
driftFactor: 0.01,
retryCount: 25,
retryDelay: Duration.fromMilliSeconds(400),
retryJitter: Duration.fromMilliSeconds(200)
};
this._isDisposed = false;
this._disposePromise = null;
given(redisClient, "redisClient").ensureHasValue().ensureIsObject();
given(config, "config").ensureIsObject().ensureHasStructure({
"driftFactor?": "number",
"retryCount?": "number",
"retryDelay?": "object",
"retryJitter?": "object"
});
this._executer = new _RedisScriptExecuter(redisClient, Object.assign(Object.assign({}, this._defaultConfig), config == null ? {} : config));
}
async lock(key, ttlDuration) {
given(key, "key").ensureHasValue().ensureIsString();
key = `n-data-dlock-${key.trim().toLowerCase()}`;
given(ttlDuration, "ttlDuration").ensureIsObject();
if (this._isDisposed)
throw new ObjectDisposedException(this);
return this._executer.lock(key, ttlDuration);
}
dispose() {
if (!this._isDisposed) {
this._isDisposed = true;
this._disposePromise = Promise.resolve();
}
return this._disposePromise;
}
};
__setFunctionName(_classThis, "RedisDistributedLockService");
(() => {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
RedisDistributedLockService = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
__runInitializers(_classThis, _classExtraInitializers);
})();
return RedisDistributedLockService = _classThis;
})();
export { RedisDistributedLockService };
class RedisDistributedLock {
get key() { return this._key; }
get value() { return this._value; }
get expiration() { return this._expiration; }
constructor(executer, key, value, expiration) {
given(executer, "executer").ensureHasValue().ensureIsObject();
this._executer = executer;
given(key, "key").ensureHasValue().ensureIsString();
this._key = key;
given(value, "value").ensureHasValue().ensureIsString();
this._value = value;
given(expiration, "expiration").ensureHasValue().ensureIsObject();
this._expiration = expiration;
}
release() {
return this._executer.release(this);
}
}
class _RedisScriptExecuter {
constructor(client, config) {
this._lockScript = `
-- Return 0 if an key already exists 1 otherwise.
-- ARGV[1] = value ARGV[2] = duration
if redis.call("exists", KEYS[1]) == 1 then
return 0
end
redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
return 1
`;
this._releaseScript = `
-- Return 0 if key is already deleted or expired 1 otherwise.
-- ARGV[1] = value for the key
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
`;
given(client, "client").ensureHasValue().ensureIsObject();
this._client = client;
given(config, "config").ensureHasValue().ensureIsObject();
this._config = config;
this._lockScriptHash = this._hashScript(this._lockScript);
this._releaseScriptHash = this._hashScript(this._releaseScript);
}
async lock(key, ttlDuration) {
given(key, "key").ensureHasValue().ensureIsString();
given(ttlDuration, "ttlDuration").ensureIsObject();
const randomValue = Uuid.create();
const duration = ttlDuration !== null && ttlDuration !== void 0 ? ttlDuration : Duration.fromSeconds(30);
let attempts = 0;
while (attempts < this._config.retryCount) {
const result = await this._executeScript(this._lockScriptHash, this._lockScript, key, randomValue, duration.toMilliSeconds().toString());
if (result)
return new RedisDistributedLock(this, key, randomValue, duration);
attempts++;
const delayDuration = this._config.retryDelay.toMilliSeconds()
+ Make.randomInt(0, this._config.retryJitter.toMilliSeconds());
await Delay.milliseconds(delayDuration);
}
throw new UnableToAcquireDistributedLockException(key);
}
async release(lock) {
given(lock, "lock").ensureHasValue().ensureIsObject();
await this._executeScript(this._releaseScriptHash, this._releaseScript, lock.key, lock.value);
}
async _executeScript(scriptHash, script, key, ...args) {
given(scriptHash, "scriptHash").ensureHasValue().ensureIsString();
given(script, "script").ensureHasValue().ensureIsString();
given(key, "key").ensureHasValue().ensureIsString();
given(args, "args").ensureHasValue().ensureIsArray();
let result;
try {
// Attempt to evaluate the script by its hash.
const hashResult = await this._client.evalSha(scriptHash, {
keys: [key],
arguments: args
});
if (typeof hashResult !== "number")
throw new ApplicationException(`Unexpected result of type ${typeof hashResult} returned from redis when executing 'evalSha' ${hashResult}.`);
result = hashResult;
}
catch (error) {
if (error instanceof Error && error.message.startsWith("NOSCRIPT")) {
const rawResult = await this._client.eval(script, {
keys: [key],
arguments: args
});
if (typeof rawResult !== "number")
throw new ApplicationException(`Unexpected result of type ${typeof rawResult} returned from redis when executing 'eval' ${rawResult}.`);
result = rawResult;
}
else
throw error;
}
return result === 1;
}
_hashScript(script) {
given(script, "script").ensureHasValue().ensureIsString();
return createHash("sha1")
.update(script).digest("hex");
}
}
export class UnableToAcquireDistributedLockException extends ApplicationException {
constructor(key) {
super(`Unable to acquire distributed lock for key '${key}'`);
}
}
//# sourceMappingURL=redis-distributed-lock-service.js.map