UNPKG

redis-smq

Version:

A high-performance, reliable, and scalable message queue for Node.js.

307 lines 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MessageHandlerRunner = void 0; const redis_smq_common_1 = require("redis-smq-common"); const index_js_1 = require("../../errors/index.js"); const message_handler_js_1 = require("../message-handler/message-handler.js"); const event_publisher_js_1 = require("./event-publisher.js"); const queue_state_change_handler_js_1 = require("./queue-state-change-handler.js"); const _validate_operation_js_1 = require("../../queue-operation-validator/_/_validate-operation.js"); const with_shared_pool_connection_js_1 = require("../../common/redis/redis-connection-pool/with-shared-pool-connection.js"); const index_js_2 = require("../../queue-operation-validator/index.js"); class MessageHandlerRunner extends redis_smq_common_1.Runnable { constructor(consumerContext) { super(); this.handlerReconciliationInterval = 5000; this.messageHandlerInstances = []; this.messageHandlers = []; this.scheduleReconciliation = () => { if (this.isOperational()) { this.supervisorTimer.schedule(this.reconcileHandlers, this.handlerReconciliationInterval); } }; this.reconcileHandlers = () => { if (!this.isOperational()) return; const runningQueues = new Set(this.messageHandlerInstances.map((i) => this.getQueueIdentifier(i.getQueue()))); const zombieHandlers = this.messageHandlers.filter((i) => !runningQueues.has(this.getQueueIdentifier(i.queue)) && this.isQueueActive(i.queue)); if (zombieHandlers.length > 0) { this.logger.warn(`Found ${zombieHandlers.length} zombie handler(s). Attempting to restart sequentially...`); const tasks = zombieHandlers.map((handlerParams) => { return (done) => { if (!this.getMessageHandler(handlerParams.queue)) { this.logger.warn(`Handler for queue ${handlerParams.queue.queueParams.name} was removed during reconciliation. Skipping restart.`); return done(); } if (!this.isQueueActive(handlerParams.queue)) { this.logger.debug(`Queue ${handlerParams.queue.queueParams.name} is not active, skipping restart`); return done(); } this.logger.debug(`Reconciling handler for queue: ${handlerParams.queue.queueParams.name}`); this.runMessageHandler(handlerParams, (err) => { if (err) { this.logger.error(`Failed to restart zombie handler for queue ${handlerParams.queue.queueParams.name}.`); } done(); }); }; }); redis_smq_common_1.async.series(tasks, () => { this.logger.debug('Finished reconciliation series for this tick.'); this.scheduleReconciliation(); }); } else { this.scheduleReconciliation(); } }; this.runMessageHandlers = (cb) => { const handlersToStart = this.messageHandlers.filter((handler) => this.isQueueActive(handler.queue)); redis_smq_common_1.async.each(handlersToStart, (handlerParams, _, done) => { this.runMessageHandler(handlerParams, done); }, cb); }; this.shutDownMessageHandlers = (cb) => { redis_smq_common_1.async.each(this.messageHandlerInstances, (handler, _, done) => { this.shutdownMessageHandler(handler, done); }, () => { this.messageHandlerInstances = []; cb(); }); }; this.consumerContext = consumerContext; this.logger = this.consumerContext.logger.createLogger(this.constructor.name); this.queueStateChangeHandler = new queue_state_change_handler_js_1.QueueStateChangeHandler(this, this.logger); (0, event_publisher_js_1.eventPublisher)(this); this.supervisorTimer = new redis_smq_common_1.Timer(this.logger); this.logger.debug(`MessageHandlerRunner with ID: ${this.id} initialized.`); } getQueueIdentifier(queue) { var _a; return `${queue.queueParams.ns}:${queue.queueParams.name}:${(_a = queue.groupId) !== null && _a !== void 0 ? _a : ''}`; } isSameQueue(q1, q2) { return this.getQueueIdentifier(q1) === this.getQueueIdentifier(q2); } isQueueActive(queue) { return this.queueStateChangeHandler.isQueueActive(queue.queueParams); } isQueueStopped(queue) { return this.queueStateChangeHandler.isQueueStopped(queue.queueParams); } isQueuePaused(queue) { return this.queueStateChangeHandler.isQueuePaused(queue.queueParams); } isQueueLocked(queue) { return this.queueStateChangeHandler.isQueueLocked(queue.queueParams); } getMessageHandlerInstance(queue) { return this.messageHandlerInstances.find((i) => this.isSameQueue(i.getQueue(), queue)); } getMessageHandler(queue) { return this.messageHandlers.find((i) => this.isSameQueue(i.queue, queue)); } createMessageHandlerInstance(handlerParams) { const instance = new message_handler_js_1.MessageHandler(this.consumerContext, handlerParams, true); instance.on('consumer.messageHandler.error', (err) => { this.logger.error(`MessageHandler [${instance.getId()}] has experienced a runtime error: ${err.message}. Shutting down instance. The supervisor will attempt to restart it.`); this.shutdownMessageHandler(instance, (err) => { if (err) { this.logger.error(`Failed to shutdown handler ${instance.getId()}: ${err.message}`); } }); }); this.messageHandlerInstances.push(instance); return instance; } runMessageHandler(handlerParams, cb) { if (!this.isQueueActive(handlerParams.queue)) { this.logger.debug(`Queue ${handlerParams.queue.queueParams.name} is not active, skipping start`); return cb(); } if (this.getMessageHandlerInstance(handlerParams.queue)) { this.logger.warn(`A message handler instance for queue ${handlerParams.queue.queueParams.name} is already running.`); return cb(); } const handler = this.createMessageHandlerInstance(handlerParams); handler.run((err) => { if (err) { this.logger.error(`Failed to run message handler for queue ${handlerParams.queue.queueParams.name}. Removing configuration.`, err); this.removeMessageHandler(handlerParams.queue, () => cb(err)); } else { cb(); } }); } shutdownMessageHandler(messageHandler, cb) { messageHandler.shutdown(() => { this.messageHandlerInstances = this.messageHandlerInstances.filter((handler) => handler.getId() !== messageHandler.getId()); cb(); }); } goingUp() { return super.goingUp().concat([ (cb) => this.supervisorTimer.run(cb), this.runMessageHandlers, (cb) => { this.reconcileHandlers(); cb(); }, ]); } goingDown() { return [ (cb) => this.supervisorTimer.shutdown(cb), this.queueStateChangeHandler.shutdown, this.shutDownMessageHandlers, ].concat(super.goingDown()); } handleError(err) { if (!this.isOperational()) return; this.logger.error(`MessageHandlerRunner error: ${err.message}`, err); this.emit('consumer.messageHandlerRunner.error', err, this.consumerContext.consumerId); super.handleError(err); } stopMessageHandler(queue, cb) { const handlerInstance = this.getMessageHandlerInstance(queue); if (!handlerInstance) { const hasConfig = !!this.getMessageHandler(queue); this.logger.debug(`Stop requested for queue: ${queue.queueParams.name} (no instance)`); return cb(null, hasConfig); } this.shutdownMessageHandler(handlerInstance, (err) => { if (err) { this.logger.error(`Failed to stop handler for queue ${queue.queueParams.name}:`, err); cb(err); } else { this.logger.debug(`Stopped message handler for queue: ${queue.queueParams.name} (configuration kept)`); cb(null, true); } }); } startMessageHandler(queue, cb) { const handlerConfig = this.getMessageHandler(queue); if (!handlerConfig) { this.logger.warn(`No handler configuration found for queue: ${queue.queueParams.name}`); return cb(null, false); } if (this.getMessageHandlerInstance(queue)) { this.logger.debug(`Handler already running for queue: ${queue.queueParams.name}`); return cb(null, false); } if (!this.isQueueActive(queue)) { this.logger.debug(`Queue ${queue.queueParams.name} is not active, cannot start handler`); return cb(null, false); } this.runMessageHandler(handlerConfig, (err) => { if (err) { this.logger.error(`Failed to start handler for queue ${queue.queueParams.name}:`, err); cb(err); } else { this.logger.debug(`Started message handler for queue: ${queue.queueParams.name}`); cb(null, true); } }); } removeMessageHandler(queue, cb) { const hadConfig = this.getMessageHandler(queue); this.messageHandlers = this.messageHandlers.filter((h) => !this.isSameQueue(h.queue, queue)); const handlerInstance = this.getMessageHandlerInstance(queue); if (handlerInstance) { this.shutdownMessageHandler(handlerInstance, cb); } else { if (hadConfig) { this.logger.debug(`Removed handler configuration for queue: ${queue.queueParams.name}`); } cb(); } } addMessageHandler(queue, messageHandler, cb) { if (this.getMessageHandler(queue)) { this.logger.warn(`Message handler for queue ${queue.queueParams.name} already exists`); return cb(new index_js_1.MessageHandlerAlreadyExistsError()); } redis_smq_common_1.async.series([ (cb) => (0, with_shared_pool_connection_js_1.withSharedPoolConnection)((client, cb) => { (0, _validate_operation_js_1._validateOperation)(client, queue.queueParams, index_js_2.EQueueOperation.CONSUME, cb); }, cb), (cb) => { const handlerParams = { queue, messageHandler, }; this.messageHandlers.push(handlerParams); this.logger.debug(`Message handler registered for queue: ${queue.queueParams.name}. Total handlers: ${this.messageHandlers.length}`); if (this.isOperational() && this.isQueueActive(queue)) { this.runMessageHandler(handlerParams, cb); } else { cb(); } }, ], (err) => cb(err)); } getQueueWithStatus() { const queues = this.getQueues(); return queues.map((queue) => { var _a; const status = ((_a = this.getMessageHandlerInstance(queue)) === null || _a === void 0 ? void 0 : _a.isRunning()) ? 'active' : 'stopped'; return { queue, status, }; }); } getQueues() { return this.messageHandlers.map((i) => i.queue); } getActiveQueues() { return this.messageHandlers .filter((handler) => this.isQueueActive(handler.queue)) .map((i) => i.queue); } getStoppedQueues() { return this.messageHandlers .filter((handler) => this.isQueueStopped(handler.queue)) .map((i) => i.queue); } getPausedQueues() { return this.messageHandlers .filter((handler) => this.isQueuePaused(handler.queue)) .map((i) => i.queue); } getLockedQueues() { return this.messageHandlers .filter((handler) => this.isQueueLocked(handler.queue)) .map((i) => i.queue); } isMessageHandlerStopped(queue) { return this.isQueueStopped(queue); } isMessageHandlerRunning(queue) { const instance = this.getMessageHandlerInstance(queue); return !!instance && instance.isOperational(); } getHandlerCount() { const total = this.messageHandlers.length; const active = this.getActiveQueues().length; const stopped = this.getStoppedQueues().length; const paused = this.getPausedQueues().length; const locked = this.getLockedQueues().length; const accountedFor = active + stopped + paused + locked; if (accountedFor !== total) { this.logger.warn(`Queue state accounting mismatch: total=${total}, accounted=${accountedFor}`); } return { total, active, stopped, paused, locked }; } } exports.MessageHandlerRunner = MessageHandlerRunner; //# sourceMappingURL=message-handler-runner.js.map