redis-smq
Version:
A high-performance, reliable, and scalable message queue for Node.js.
359 lines • 16.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConsumeMessage = void 0;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const redis_smq_common_1 = require("redis-smq-common");
const redis_keys_js_1 = require("../../../common/redis/redis-keys/redis-keys.js");
const index_js_1 = require("./types/index.js");
const index_js_2 = require("../../../errors/index.js");
const message_unacknowledger_js_1 = require("./message-unacknowledger.js");
const event_publisher_js_1 = require("./event-publisher.js");
const connection_pool_js_1 = require("../../../common/redis/redis-connection-pool/types/connection-pool.js");
const redis_connection_pool_js_1 = require("../../../common/redis/redis-connection-pool/redis-connection-pool.js");
const message_acknowledger_js_1 = require("./message-acknowledger.js");
const isCallbackHandler = (handler) => {
return handler.length === 2;
};
const isPromiseHandler = (handler) => {
return handler.length === 1;
};
class ConsumeMessage extends redis_smq_common_1.Runnable {
constructor(consumerContext, queue, messageHandlerId, messageHandler) {
super();
this.redisClient = null;
this.consumeMessageWorker = null;
this.validateHandler = (cb) => {
const messageHandler = this.messageHandler;
if (typeof messageHandler === 'function') {
if (![1, 2].includes(this.messageHandler.length)) {
this.logger.debug('Function-based handler has invalid signature');
return cb(new index_js_2.InvalidMessageHandlerSignatureError({
metadata: {
queue: this.queue,
},
}));
}
return cb();
}
if (typeof messageHandler === 'string') {
const ext = path_1.default.extname(messageHandler);
if (!['.js', '.cjs'].includes(ext)) {
this.logger.error(`Invalid extension: ${ext}`);
return cb(new index_js_2.MessageHandlerFilenameExtensionError());
}
return (0, fs_1.stat)(messageHandler, (err) => {
if (err) {
this.logger.error(`Handler file not found: ${messageHandler}`);
return cb(new index_js_2.MessageHandlerFileError());
}
this.logger.debug(`Handler validated: ${messageHandler}`);
cb();
});
}
this.logger.debug('Invalid message handler type');
return cb(new index_js_2.InvalidMessageHandlerTypeError({
metadata: {
queue: this.queue,
},
}));
};
this.consumerId = consumerContext.consumerId;
this.logger = consumerContext.logger.createLogger(this.constructor.name);
this.config = consumerContext.config;
this.consumerOptions = consumerContext.consumerOptions;
this.queue = queue;
this.messageHandler = messageHandler;
this.messageHandlerId = messageHandlerId;
this.messageAcknowledger = this.initializeMessageAcknowledger();
this.messageUnacknowledger = this.initializeMessageUnacknowledger();
const { keyQueueProcessing } = redis_keys_js_1.redisKeys.getQueueConsumerKeys(this.queue.queueParams, this.consumerId);
const { keyQueueProperties, keyQueueAcknowledged } = redis_keys_js_1.redisKeys.getQueueKeys(this.queue.queueParams.ns, this.queue.queueParams.name, this.queue.groupId);
this.keyQueueProperties = keyQueueProperties;
this.keyQueueAcknowledged = keyQueueAcknowledged;
this.keyQueueProcessing = keyQueueProcessing;
(0, event_publisher_js_1.eventPublisher)(this);
this.logger.debug(`${this.constructor.name} initialized for queue ${queue.queueParams.name}@${queue.queueParams.ns}`);
}
initializeMessageAcknowledger() {
const messageAcknowledger = new message_acknowledger_js_1.MessageAcknowledger(this.queue, this.consumerId, this.logger, this.consumerOptions);
messageAcknowledger.on('messageAcknowledger.error', (err) => {
this.handleError(err);
});
messageAcknowledger.on('messageAcknowledger.messageAcknowledged', (message) => {
this.onMessageAcknowledged(message);
});
return messageAcknowledger;
}
initializeMessageUnacknowledger() {
const messageUnacknowledger = new message_unacknowledger_js_1.MessageUnacknowledger(this.consumerId, this.queue, this.logger, this.consumerOptions);
messageUnacknowledger.on('messageUnacknowledger.error', (err) => {
this.handleError(err);
});
messageUnacknowledger.on('messageUnacknowledger.messagesUnacknowledged', (status) => {
this.handleUnacknowledgementResult(status);
});
return messageUnacknowledger;
}
handleReceivedMessage(message) {
const messageId = message.getId();
this.logger.debug(`Received message ${messageId}`);
if (!this.isOperational()) {
this.logger.warn(`Ignoring message ${messageId} - consumer not running`);
return;
}
if (message.getSetExpired()) {
this.logger.info(`Message ${messageId} expired, unacknowledging`);
this.unacknowledgeMessage(message, index_js_1.EMessageUnacknowledgementCause.TTL_EXPIRED);
}
else {
this.logger.debug(`Message ${messageId} valid, proceeding with consumption`);
this.consumeMessage(message);
}
}
acknowledgeMessage(message) {
if (this.messageAcknowledger) {
this.messageAcknowledger.add(message);
}
else {
this.logger.error('MessageAcknowledger not initialized');
}
this.emit('consumer.consumeMessage.next');
}
unacknowledgeMessage(message, cause) {
this.messageUnacknowledger.add(message, cause);
this.emit('consumer.consumeMessage.next');
}
onMessageAcknowledged(message) {
const messageId = message.getId();
this.logger.info(`Message ${messageId} acknowledged successfully`);
this.emit('consumer.consumeMessage.messageAcknowledged', messageId, this.queue, this.messageHandlerId, this.consumerId);
}
handleUnacknowledgementResult(result) {
const messageCount = Object.keys(result).length;
if (messageCount === 0)
return;
this.logger.debug(`Handling unacknowledgement result for ${messageCount} messages`);
for (const [messageId, details] of Object.entries(result)) {
this.logger.info(`Message ${messageId} unacknowledged: ${index_js_1.EMessageUnacknowledgementCause[details.cause]}`);
this.emit('consumer.consumeMessage.messageUnacknowledged', messageId, this.queue, this.messageHandlerId, this.consumerId, details.cause);
if (details.action === index_js_1.EMessageUnacknowledgementAction.DEAD_LETTER) {
this.logger.info(`Message ${messageId} moved to dead letter queue`);
this.emit('consumer.consumeMessage.messageDeadLettered', messageId, this.queue, this.messageHandlerId, this.consumerId, details.deadLetterCause);
}
else if (details.action === index_js_1.EMessageUnacknowledgementAction.DELAY) {
this.logger.info(`Message ${messageId} delayed for retry`);
this.emit('consumer.consumeMessage.messageDelayed', messageId, this.queue, this.messageHandlerId, this.consumerId);
}
else {
this.logger.info(`Message ${messageId} re-queued for retry`);
this.emit('consumer.consumeMessage.messageRequeued', messageId, this.queue, this.messageHandlerId, this.consumerId);
}
}
}
consumeMessage(message) {
const messageId = message.getId();
this.logger.debug(`Consuming message ${messageId}`);
let isCallbackHandled = false;
try {
let onConsumed = (err) => {
if (isCallbackHandled) {
this.logger.debug(`Callback for ${messageId} already handled`);
return;
}
isCallbackHandled = true;
if (!this.isOperational()) {
this.logger.debug(`Consumer not running, ignoring callback for ${messageId}`);
return;
}
if (err) {
this.logger.error(`Error consuming ${messageId}: ${err.message}`);
let reason = index_js_1.EMessageUnacknowledgementCause.UNACKNOWLEDGED;
if (err instanceof redis_smq_common_1.AsyncCallbackTimeoutError) {
reason = index_js_1.EMessageUnacknowledgementCause.TIMEOUT;
}
if (err instanceof index_js_2.InvalidMessageHandlerSignatureError) {
reason = index_js_1.EMessageUnacknowledgementCause.INVALID_HANDLER_SIGNATURE;
}
this.unacknowledgeMessage(message, reason);
return;
}
this.logger.debug(`Message ${messageId} consumed successfully`);
this.acknowledgeMessage(message);
};
const consumeTimeout = message.producibleMessage.getConsumeTimeout();
if (consumeTimeout) {
this.logger.debug(`Setting timeout ${consumeTimeout}ms for ${messageId}`);
onConsumed = redis_smq_common_1.async.withTimeout(onConsumed, consumeTimeout);
}
this.invokeMessageHandler(this.messageHandler, message.transfer(), onConsumed);
}
catch (error) {
this.logger.error(`Exception consuming ${messageId}: ${error}`);
isCallbackHandled = true;
this.unacknowledgeMessage(message, index_js_1.EMessageUnacknowledgementCause.CONSUME_ERROR);
this.emit('consumer.consumeMessage.next');
}
}
invokeMessageHandler(handler, msg, cb) {
const messageId = msg.id;
if (typeof handler === 'string') {
this.getWorker(handler).call(msg, (err) => {
if (err) {
this.logger.error(`Worker handler failed for ${messageId}: ${err.message}`);
}
cb(err);
});
return;
}
let isCompleted = false;
const complete = (err) => {
if (isCompleted)
return;
isCompleted = true;
if (err) {
this.logger.error(`Handler failed for ${messageId}: ${err.message}`);
cb(err);
}
else {
this.logger.debug(`Handler succeeded for ${messageId}`);
cb(null);
}
};
try {
if (isPromiseHandler(handler)) {
const result = handler(msg);
if (result && typeof result.then === 'function') {
result
.then(() => complete())
.catch((err) => {
const error = err instanceof Error
? err
: new Error(String(err !== null && err !== void 0 ? err : 'Promise rejected'));
complete(error);
});
}
else {
this.logger.warn(`Handler for ${messageId} didn't return a Promise`);
complete();
}
}
else if (isCallbackHandler(handler)) {
handler(msg, (err) => {
complete(err !== null && err !== void 0 ? err : undefined);
});
}
else {
this.logger.error(`Handler for ${messageId} has invalid signature`);
complete(new index_js_2.InvalidMessageHandlerSignatureError({
metadata: {
queue: this.queue,
},
}));
}
}
catch (err) {
const error = err instanceof Error
? err
: new Error(String(err || 'Handler threw error'));
complete(error);
}
}
getWorker(filename) {
if (!this.consumeMessageWorker) {
this.logger.debug(`Creating worker for ${filename}`);
this.consumeMessageWorker = new redis_smq_common_1.CallableWorker(filename, this.logger);
}
return this.consumeMessageWorker;
}
goingUp() {
return super.goingUp().concat([
(cb) => {
this.logger.debug('Initializing Redis client');
redis_connection_pool_js_1.RedisConnectionPool.getInstance().acquire(connection_pool_js_1.ERedisConnectionAcquisitionMode.SHARED, (err, client) => {
if (err) {
this.logger.error(`Redis client failed: ${err.message}`);
return cb(err);
}
if (!client)
return cb(new redis_smq_common_1.CallbackEmptyReplyError());
this.redisClient = client;
cb();
});
},
(cb) => {
this.logger.debug('Starting MessageUnacknowledger');
this.messageUnacknowledger.run((err) => {
if (err) {
this.logger.error(`Failed to start MessageUnacknowledger: ${err.message}`);
return cb(err);
}
this.logger.debug('MessageUnacknowledger started');
cb();
});
},
(cb) => {
this.messageAcknowledger.run((err) => {
if (err) {
this.logger.error(`Failed to start MessageAcknowledger: ${err.message}`);
return cb(err);
}
this.logger.debug('MessageAcknowledger started');
cb();
});
},
(cb) => {
this.logger.debug('Validating message handler');
this.validateHandler(cb);
},
]);
}
goingDown() {
return [
(cb) => {
if (this.messageAcknowledger) {
this.logger.info('Shutting down MessageAcknowledger');
this.messageAcknowledger.shutdown(cb);
}
else {
cb();
}
},
(cb) => {
this.logger.debug('Shutting down MessageUnacknowledger');
this.messageUnacknowledger.shutdown(cb);
},
(cb) => {
if (this.consumeMessageWorker) {
this.consumeMessageWorker.shutdown((err) => {
if (err)
this.logger.warn(`Worker shutdown error: ${err.message}`);
cb();
});
}
else {
cb();
}
},
(cb) => {
if (this.redisClient) {
redis_connection_pool_js_1.RedisConnectionPool.getInstance().release(this.redisClient);
this.redisClient = null;
}
cb();
},
].concat(super.goingDown());
}
handleError(err) {
if (!this.isOperational())
return;
const error = err instanceof Error ? err : new Error(String(err));
this.emit('consumer.consumeMessage.error', error, this.consumerId, this.queue);
super.handleError(err);
}
}
exports.ConsumeMessage = ConsumeMessage;
//# sourceMappingURL=consume-message.js.map