redis-smq-common
Version:
Provides essential components and utilities shared across RedisSMQ packages.
237 lines • 9.74 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedisLock = void 0;
const path_1 = require("path");
const index_js_1 = require("../env/index.js");
const index_js_2 = require("../errors/index.js");
const index_js_3 = require("./errors/index.js");
const index_js_4 = require("../runnable/index.js");
const index_js_5 = require("../timer/index.js");
const index_js_6 = require("../backoff/index.js");
const with_optional_callback_js_1 = require("../async/with-optional-callback.js");
const dir = index_js_1.env.getCurrentDir();
var ELuaScript;
(function (ELuaScript) {
ELuaScript["RELEASE_LOCK"] = "RELEASE_LOCK";
ELuaScript["EXTEND_LOCK"] = "EXTEND_LOCK";
})(ELuaScript || (ELuaScript = {}));
const luaScriptMap = {
[ELuaScript.RELEASE_LOCK]: (0, path_1.resolve)(dir, './lua-scripts/release-lock.lua'),
[ELuaScript.EXTEND_LOCK]: (0, path_1.resolve)(dir, './lua-scripts/extend-lock.lua'),
};
class RedisLock extends index_js_4.Runnable {
constructor(redisClient, logger, lockKey, ttl, retryOnFail = false, autoExtend = false) {
super();
this.autoExtendInterval = 0;
this.backoff = null;
this.lock = (cb) => {
this.logger.debug(`Attempting to acquire lock for key: ${this.lockKey}`);
const execFn = (done) => {
this.redisClient.set(this.lockKey, this.id, {
expire: { mode: 'PX', value: this.ttl },
exists: 'NX',
}, (err, reply) => {
if (!this.isGoingUp()) {
this.logger.warn('Lock acquisition aborted: instance is no longer in going-up state');
return done(new index_js_2.AbortError());
}
if (err) {
this.logger.error(`Error acquiring lock: ${err.message}`, err);
return done(new index_js_2.AbortError({
message: err.message,
}));
}
if (!reply) {
this.logger.warn('Failed to acquire lock: already held by another instance');
return done(new index_js_3.LockNotAcquiredError());
}
this.logger.debug(`Lock acquired successfully for key: ${this.lockKey}`);
done();
});
};
if (this.backoff) {
return this.backoff.execute(execFn, cb);
}
execFn(cb);
};
this.extend = (cb) => {
if (!this.isRunning()) {
this.logger.warn('Cannot extend lock: lock is not currently held');
return cb(new index_js_3.NotLockedError());
}
this.redisClient.runScript(ELuaScript.EXTEND_LOCK, [this.lockKey], [this.id, this.ttl], (err, reply) => {
if (err) {
this.logger.error(`Error extending lock TTL: ${err.message}`, err);
return cb(err);
}
if (!this.isRunning()) {
this.logger.warn('Lock extension aborted: instance is no longer in running state');
return cb(new index_js_2.AbortError());
}
if (reply !== 1) {
this.logger.warn('Failed to extend lock: lock no longer held by this instance');
return this.shutdown(() => cb(new index_js_3.ExtendLockError()));
}
cb();
});
};
this.release = (cb) => {
this.logger.debug(`Attempting to release lock for key: ${this.lockKey}`);
this.redisClient.runScript(ELuaScript.RELEASE_LOCK, [this.lockKey], [this.id], (err) => {
if (err) {
this.logger.error(`Error releasing lock: ${err.message}`, err);
}
else {
this.logger.debug(`Lock released successfully for key: ${this.lockKey}`);
}
cb(err);
});
};
this.handleError = (err) => {
if (!this.isOperational())
return;
this.logger.error(`RedisLock error: ${err.message}`, err);
this.emit('locker.error', err, this.id);
super.handleError(err);
};
this.lockKey = lockKey;
if (ttl < 3000) {
throw new Error(`Lock TTL is too small`);
}
this.ttl = ttl;
if (autoExtend) {
this.autoExtendInterval = Math.floor(ttl / 3);
}
this.logger = logger.createLogger(this.constructor.name);
this.timer = new index_js_5.Timer(this.logger);
this.redisClient = redisClient;
this.redisClient.on('error', this.handleError);
if (retryOnFail) {
this.backoff = new index_js_6.ExponentialBackoff(logger, { maxAttempts: 0 });
}
this.logger.debug('RedisLock initialization complete');
}
autoExtendLock() {
if (!this.autoExtendInterval || !this.isRunning()) {
return;
}
this.timer.schedule(() => {
this.extend((err) => {
if (err) {
if (err instanceof index_js_2.AbortError)
return;
this.handleError(err);
}
this.autoExtendLock();
});
}, this.autoExtendInterval);
}
goingUp() {
this.logger.debug('RedisLock transitioning to going-up state');
this.emit('locker.goingUp', this.id);
return super.goingUp().concat([
(cb) => this.timer.run(cb),
(cb) => {
if (this.backoff)
this.backoff.run(cb);
else
cb();
},
(cb) => {
this.logger.debug('Loading Redis Lua scripts');
this.redisClient.loadScriptFiles(luaScriptMap, (err) => {
if (err) {
this.logger.error(`Failed to load Redis Lua scripts: ${err.message}`, err);
this.handleError(err);
}
else {
this.logger.debug('Redis Lua scripts loaded successfully');
}
cb(err);
});
},
this.lock,
]);
}
goingDown() {
this.logger.debug('RedisLock transitioning to going-down state');
this.emit('locker.goingDown', this.id);
return [
(cb) => {
if (this.backoff)
this.backoff.shutdown(cb);
else
cb();
},
this.release,
].concat(super.goingDown());
}
finalizeUp() {
super.finalizeUp();
this.logger.debug(`RedisLock transitioned to up state for key: ${this.lockKey}`);
this.emit('locker.up', this.id);
if (this.autoExtendInterval) {
this.logger.debug(`Auto-extension enabled with interval: ${this.autoExtendInterval}ms`);
this.autoExtendLock();
}
}
finalizeDown() {
super.finalizeDown();
this.logger.debug(`RedisLock transitioned to down state for key: ${this.lockKey}`);
this.emit('locker.down', this.id);
}
run(cb) {
(0, with_optional_callback_js_1.withOptionalCallback)(cb, (callback) => {
this.logger.debug(`Attempting to run RedisLock for key: ${this.lockKey}`);
super.run((err) => {
if (err instanceof index_js_3.LockNotAcquiredError) {
this.logger.debug(`Lock already held by another instance for key: ${this.lockKey}`);
return callback(err);
}
if (err) {
this.logger.error(`Error running RedisLock: ${err.message}`, err);
return callback(err);
}
this.logger.debug(`RedisLock running successfully for key: ${this.lockKey}`);
callback(null);
});
});
}
acquireLock(cb) {
this.logger.debug(`Acquiring lock for key: ${this.lockKey}`);
this.run(cb);
}
releaseLock(cb) {
this.logger.debug(`Releasing lock for key: ${this.lockKey}`);
this.shutdown(cb);
}
extendLock(cb) {
this.logger.debug(`Manual extension of lock requested for key: ${this.lockKey}`);
if (this.autoExtendInterval) {
this.logger.warn('Cannot manually extend lock: auto-extension is enabled');
return cb(new index_js_3.AcquireLockNotAllowedError());
}
if (!this.isRunning()) {
this.logger.warn('Cannot extend lock: lock is not currently held');
return cb(new index_js_3.NotLockedError());
}
this.logger.debug('Extending lock TTL');
this.extend(cb);
}
isLocked() {
const locked = this.powerSwitch.isRunning();
this.logger.debug(`Lock status check for key ${this.lockKey}: ${locked ? 'locked' : 'not locked'}`);
return locked;
}
isReleased() {
const released = this.powerSwitch.isDown();
this.logger.debug(`Lock release status check for key ${this.lockKey}: ${released ? 'released' : 'not released'}`);
return released;
}
getRetryAttempts() {
var _a, _b;
return (_b = (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.getAttempts()) !== null && _b !== void 0 ? _b : 0;
}
}
exports.RedisLock = RedisLock;
//# sourceMappingURL=redis-lock.js.map