UNPKG

redis-smq

Version:

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

229 lines 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MessageUnacknowledger = void 0; const redis_smq_common_1 = require("redis-smq-common"); const redis_keys_js_1 = require("../../../common/redis/redis-keys/redis-keys.js"); const _get_message_js_1 = require("../../../message-manager/_/_get-message.js"); const index_js_1 = require("./types/index.js"); const with_shared_pool_connection_js_1 = require("../../../common/redis/redis-connection-pool/with-shared-pool-connection.js"); const _execute_unacknowledgement_script_js_1 = require("./_/_execute-unacknowledgement-script.js"); class MessageUnacknowledger extends redis_smq_common_1.Runnable { constructor(consumerId, queue, logger, consumerOptions) { super(); this.consumerId = consumerId; this.queue = queue; this.queueRef = `${queue.queueParams.name}@${queue.queueParams.ns}/${queue.groupId}`; this.logger = logger.createLogger(`${this.constructor.name.toLowerCase()}`); const { enabled, batchTimeoutMs, batchSize } = consumerOptions.batchUnacks; this.useBatchUnacks = enabled; this.batchSize = batchSize; this.batchTimeoutMs = batchTimeoutMs; this.batch = { messages: [], callbacks: [], timer: null, }; this.logger.debug(`MessageUnacknowledger initialized for queue ${this.queueRef}`); } getResolution(message, cause) { if (cause === index_js_1.EMessageUnacknowledgementCause.TTL_EXPIRED) { return { cause, action: index_js_1.EMessageUnacknowledgementAction.DEAD_LETTER, deadLetterCause: index_js_1.EMessageDeadLetterCause.TTL_EXPIRED, }; } if (message.isPeriodic()) { return { cause, action: index_js_1.EMessageUnacknowledgementAction.DEAD_LETTER, deadLetterCause: index_js_1.EMessageDeadLetterCause.PERIODIC_MESSAGE, }; } if (message.hasRetryThresholdExceeded()) { return { cause, action: index_js_1.EMessageUnacknowledgementAction.DEAD_LETTER, deadLetterCause: index_js_1.EMessageDeadLetterCause.RETRY_THRESHOLD_EXCEEDED, }; } const delay = message.producibleMessage.getRetryDelay(); return delay ? { cause, action: index_js_1.EMessageUnacknowledgementAction.DELAY } : { cause, action: index_js_1.EMessageUnacknowledgementAction.REQUEUE }; } buildPendingMessages(messages, cause) { return messages.map((m) => { const resolution = this.getResolution(m, cause); const { keyMessage } = redis_keys_js_1.redisKeys.getMessageKeys(m.getId()); return { messageId: m.getId(), keyMessage, message: m, resolution, }; }); } processBatch() { if (this.batch.timer) { clearTimeout(this.batch.timer); this.batch.timer = null; } if (this.batch.messages.length === 0) { this.batch.callbacks.forEach((cb) => cb(null, {})); this.batch.callbacks = []; return; } const messages = [...this.batch.messages]; const callbacks = [...this.batch.callbacks]; this.batch.messages = []; this.batch.callbacks = []; this.logger.debug(`Processing batch of ${messages.length} messages for queue ${this.queueRef}`); (0, _execute_unacknowledgement_script_js_1._executeUnacknowledgementScript)(this.queue, messages, this.consumerId, this.logger, (err, result) => { if (err) { callbacks.forEach((cb) => cb(err)); return this.handleError(err); } if (result) { this.emit('messageUnacknowledger.messagesUnacknowledged', result); callbacks.forEach((cb) => cb(null, result)); } }); } getMessagesFromQueue(client, cb) { const { keyQueueProcessing } = redis_keys_js_1.redisKeys.getQueueConsumerKeys(this.queue.queueParams, this.consumerId); redis_smq_common_1.async.waterfall([ (next) => { client.lrange(keyQueueProcessing, 0, -1, (err, reply) => next(err, reply !== null && reply !== void 0 ? reply : [])); }, (ids, next) => { if (!ids.length) return next(null, []); (0, _get_message_js_1._getMessages)(client, ids, next); }, ], cb); } goingDown() { return [ (next) => { if (this.batch.messages.length === 0) { this.logger.debug('No pending batch to flush during shutdown'); return next(); } this.logger.info(`Flushing pending batch of ${this.batch.messages.length} messages on shutdown`); this.flush((err) => { if (err) { this.logger.error(`Error flushing batch during shutdown: ${err.message}`); } next(); }); }, (next) => { this.logger.info('Unacknowledging messages in processing queue'); this.unacknowledgeProcessingQueue(index_js_1.EMessageUnacknowledgementCause.SHUTTING_DOWN, () => next()); }, ]; } handleError(err) { if (!this.isOperational()) return; const error = err instanceof Error ? err : new Error(String(err)); this.logger.error(`MessageUnacknowledger error: ${error.message}`); this.emit('messageUnacknowledger.error', error); super.handleError(err); } add(message, cause) { if (!this.isOperational()) { this.logger.warn(`Cannot add message ${message.getId()} - not operational`); return; } if (!this.useBatchUnacks) { this.unacknowledgeMessage(message, cause, (err, result) => { if (err) this.handleError(err); if (result) { this.emit('messageUnacknowledger.messagesUnacknowledged', result); } }); return; } const resolution = this.getResolution(message, cause); this.logger.debug(`Adding message ${message.getId()} to unacknowledger, action: ${resolution.action}`); this.batch.messages.push({ message, resolution, }); if (this.batch.messages.length >= this.batchSize) { this.logger.debug(`Batch full, flushing immediately`); this.processBatch(); } else if (!this.batch.timer) { this.logger.debug(`Setting timer: ${this.batchTimeoutMs}ms`); this.batch.timer = setTimeout(() => { if (this.batch.messages.length > 0) { this.logger.debug(`Timer expired, flushing`); this.processBatch(); } }, this.batchTimeoutMs); this.batch.timer.unref(); } } unacknowledgeMessage(message, cause, cb) { if (!this.isOperational()) { return cb(new redis_smq_common_1.PanicError({ message: 'MessageUnacknowledger not operational' })); } const resolution = this.getResolution(message, cause); this.logger.debug(`Immediately unacknowledging message ${message.getId()}`); (0, _execute_unacknowledgement_script_js_1._executeUnacknowledgementScript)(this.queue, [ { message, resolution, }, ], this.consumerId, this.logger, cb); } unacknowledgeProcessingQueue(cause, cb) { if (this.isDown() && !this.isGoingUp()) { return cb(new redis_smq_common_1.PanicError({ message: 'MessageUnacknowledger is fully down' })); } this.logger.info(`Unacknowledging all messages in processing queue ${this.queueRef}`); redis_smq_common_1.async.waterfall([ (next) => { (0, with_shared_pool_connection_js_1.withSharedPoolConnection)((client, done) => { this.getMessagesFromQueue(client, done); }, next); }, (messages, next) => { const messageCount = (messages === null || messages === void 0 ? void 0 : messages.length) || 0; this.logger.debug(`Found ${messageCount} messages in processing queue ${this.queueRef}`); const pending = this.buildPendingMessages(messages || [], cause); (0, _execute_unacknowledgement_script_js_1._executeUnacknowledgementScript)(this.queue, pending, this.consumerId, this.logger, next); }, ], (err, result) => { if (err) { this.logger.error(`Failed to unacknowledge queue ${this.queueRef}: ${err.message}`); } if (result) { const unackedCount = Object.keys(result).length; this.logger.info(`Unacknowledged ${unackedCount} messages from processing queue ${this.queueRef}`); this.emit('messageUnacknowledger.messagesUnacknowledged', result); } cb(err, result); }); } flush(cb) { if (this.isDown() && !this.isGoingUp()) { return cb === null || cb === void 0 ? void 0 : cb(new redis_smq_common_1.PanicError({ message: 'MessageUnacknowledger is fully down' })); } if (this.batch.messages.length === 0) { this.logger.debug('No pending messages to flush'); cb === null || cb === void 0 ? void 0 : cb(null, {}); return; } this.logger.debug(`Flushing ${this.batch.messages.length} pending messages`); if (cb) this.batch.callbacks.push(cb); this.processBatch(); } } exports.MessageUnacknowledger = MessageUnacknowledger; //# sourceMappingURL=message-unacknowledger.js.map