UNPKG

redis-smq-common

Version:

Provides essential components and utilities shared across RedisSMQ packages.

237 lines 9.74 kB
"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