UNPKG

redis-smq-common

Version:

Provides essential components and utilities shared across RedisSMQ packages.

172 lines 6.78 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Heartbeat = void 0; const index_js_1 = require("../runnable/index.js"); const index_js_2 = require("../errors/index.js"); const index_js_3 = require("../backoff/index.js"); const index_js_4 = require("../timer/index.js"); class Heartbeat extends index_js_1.Runnable { constructor(redisClient, logger, config, dataFn) { var _a; super(); this.beat = (cb) => { if (!this.isOperational()) { this.logger.debug('Skipping heartbeat – component not operational'); return; } this.getPayload((err, payload) => { if (err || !payload) { cb(err || new index_js_2.CallbackEmptyReplyError()); return; } if (!this.isOperational()) { this.logger.debug('Skipping Redis SET – shutdown in progress'); return; } let payloadStr; try { payloadStr = JSON.stringify(payload); } catch (serializeErr) { cb(new Error(`Failed to serialize payload: ${serializeErr}`)); return; } this.redisClient.set(this.heartbeatKey, payloadStr, { expire: { mode: 'PX', value: this.heartbeatTTL } }, (err) => { if (err) return cb(err); cb(null, payload); }); }); }; if (!config.componentId || !config.componentType || !config.heartbeatKey) { throw new Error('Heartbeat: componentId, componentType and heartbeatKey are required'); } this.redisClient = redisClient; this.componentId = config.componentId; this.componentType = config.componentType; this.heartbeatKey = config.heartbeatKey; this.logger = logger.createLogger(this.constructor.name); if (config.heartbeatTTL && config.heartbeatTTL < Heartbeat.MIN_HEARTBEAT_TTL) { throw new Error('heartbeatTTL must be not longer than 3000 milliseconds'); } this.heartbeatTTL = (_a = config.heartbeatTTL) !== null && _a !== void 0 ? _a : Heartbeat.DEFAULT_HEARTBEAT_TTL; this.heartbeatInterval = Math.floor(Math.max(1, this.heartbeatTTL / 3)); this.backoff = new index_js_3.ExponentialBackoff(this.logger, { maxAttempts: 3 }); this.timer = new index_js_4.Timer(this.logger); this.dataFn = dataFn; this.logger.debug(`Heartbeat initialized for ${this.componentType}:${this.componentId} (interval: ${this.heartbeatInterval}ms, TTL: ${this.heartbeatTTL}ms)`); } static isComponentAlive(redisClient, heartbeatKey, cb) { redisClient.get(heartbeatKey, (err, heartbeat) => { if (err) return cb(err); cb(null, !!heartbeat); }); } static areComponentsAlive(redisClient, heartbeatKeys, cb) { if (!heartbeatKeys.length) return cb(null, {}); redisClient.mget(heartbeatKeys, (err, replies = []) => { if (err) return cb(err); const result = {}; heartbeatKeys.forEach((key, index) => { result[key] = !!replies[index]; }); cb(null, result); }); } getPayload(cb) { const timestamp = Date.now(); this.dataFn((err, data) => { if (err) return cb(err); if (data == null) { return cb(new index_js_2.CallbackEmptyReplyError({ message: 'dataFn returned null/undefined', })); } cb(null, { timestamp, componentId: this.componentId, componentType: this.componentType, data, }); }); } scheduleNextBeat() { if (!this.isOperational()) { this.logger.debug('Not scheduling next beat – component not operational'); return; } this.timer.schedule(() => { this.backoff.execute(this.beat, (err, payload) => { if (err) return this.handleError(err); if (payload) { this.emit('heartbeat.beat', this.componentId, this.componentType, payload.timestamp, payload); this.scheduleNextBeat(); } }); }, this.heartbeatInterval); } handleError(err) { if (!this.isOperational()) return; this.emit('heartbeat.error', err, this.componentId, this.componentType); super.handleError(err); } finalizeUp() { super.finalizeUp(); this.emit('heartbeat.up', this.componentId, this.componentType); this.scheduleNextBeat(); } finalizeDown() { super.finalizeDown(); this.emit('heartbeat.down', this.componentId, this.componentType); } goingUp() { this.emit('heartbeat.goingUp', this.componentId, this.componentType); return super.goingUp().concat([ (cb) => this.timer.run(cb), (cb) => this.backoff.run(cb), (cb) => { const onHeartbeat = () => { cleanup(); cb(); }; const onError = (err) => { cleanup(); cb(err); }; const cleanup = () => { this.removeListener('heartbeat.beat', onHeartbeat); this.removeListener('heartbeat.error', onError); }; this.once('heartbeat.beat', onHeartbeat); this.once('heartbeat.error', onError); this.beat((err) => cb(err)); }, ]); } goingDown() { this.emit('heartbeat.goingDown', this.componentId, this.componentType); return [ (cb) => this.timer.shutdown(cb), (cb) => this.backoff.shutdown(cb), (cb) => { this.logger.debug(`Deleting heartbeat key ${this.heartbeatKey}`); this.redisClient.del(this.heartbeatKey, (err) => { if (err) this.logger.warn(`Error deleting heartbeat key: ${err.message}`); cb(); }); }, ].concat(super.goingDown()); } } exports.Heartbeat = Heartbeat; Heartbeat.MIN_HEARTBEAT_TTL = 3000; Heartbeat.DEFAULT_HEARTBEAT_TTL = Heartbeat.MIN_HEARTBEAT_TTL * 20; //# sourceMappingURL=heartbeart.js.map