redis-smq-common
Version:
Provides essential components and utilities shared across RedisSMQ packages.
172 lines • 6.78 kB
JavaScript
"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