redis-smq
Version:
A high-performance, reliable, and scalable message queue for Node.js.
247 lines • 10.5 kB
JavaScript
import { async, CallbackEmptyReplyError, createLogger, PanicError, Runnable, } from 'redis-smq-common';
import { Configuration } from '../config-manager/configuration.js';
import { _parseQueueExtendedParams } from '../queue-manager/_/_parse-queue-extended-params.js';
import { MessageHandlerRunner } from './message-handler-runner/message-handler-runner.js';
import { MultiplexedMessageHandlerRunner } from './message-handler-runner/multiplexed-message-handler-runner.js';
import { eventPublisher } from './event-publisher.js';
import { redisKeys } from '../common/redis/redis-keys/redis-keys.js';
import { HeartbeatFactory } from '../common/heartbeat/heartbeat.js';
import { heartbeatEventPublisher } from './heartbeat-event-publisher.js';
import { _parseConsumerOptions } from './_/_parse-consumer-options.js';
import { RedisConnectionPool } from '../common/redis/redis-connection-pool/redis-connection-pool.js';
import { ERedisConnectionAcquisitionMode } from '../common/redis/redis-connection-pool/types/connection-pool.js';
export class Consumer extends Runnable {
static defaultOptions = {
enableMultiplexing: false,
heartbeatTTL: 60_000,
batchAcks: {
enabled: true,
batchSize: 100,
batchTimeoutMs: 10_000,
},
batchUnacks: {
enabled: true,
batchSize: 100,
batchTimeoutMs: 10_000,
},
};
consumerContext;
messageHandlerRunner;
logger;
heartbeat = null;
consumerOptions;
redisClient = null;
constructor(consumerOptions) {
super();
this.consumerOptions = _parseConsumerOptions(consumerOptions, Consumer.defaultOptions);
const config = Configuration.getConfig();
this.logger = createLogger(config.logger, `${this.constructor.name.toLowerCase()}-${this.id}`);
this.consumerContext = {
consumerId: this.getId(),
config: config,
logger: this.logger,
consumerOptions: this.consumerOptions,
};
eventPublisher(this);
this.messageHandlerRunner = this.consumerOptions.enableMultiplexing
? new MultiplexedMessageHandlerRunner(this.consumerContext)
: new MessageHandlerRunner(this.consumerContext);
this.messageHandlerRunner.on('consumer.messageHandlerRunner.error', (err) => {
this.logger.error(`Message handler runner error: ${err.message}`);
this.handleError(err);
});
this.logger.info(`Consumer initialized${this.consumerOptions.enableMultiplexing ? ' with multiplexing enabled' : ''}`);
}
initRedisClient = (cb) => {
RedisConnectionPool.getInstance().acquire(ERedisConnectionAcquisitionMode.SHARED, (err, client) => {
if (err)
return cb(err);
if (!client)
return cb(new CallbackEmptyReplyError());
this.redisClient = client;
cb();
});
};
releaseRedisClient = (cb) => {
if (this.redisClient) {
RedisConnectionPool.getInstance().release(this.redisClient);
this.redisClient = null;
}
cb();
};
setUpHeartbeat = (cb) => {
if (!this.redisClient)
return cb(new PanicError({
message: 'Redis client is not initialized',
}));
this.logger.debug('Setting up consumer heartbeat');
const { keyConsumerHeartbeat } = redisKeys.getConsumerKeys(this.id);
try {
this.heartbeat = HeartbeatFactory(this.redisClient, this.logger, {
heartbeatKey: keyConsumerHeartbeat,
componentId: this.id,
componentType: this.constructor.name,
heartbeatTTL: this.consumerOptions.heartbeatTTL,
}, heartbeatEventPublisher);
this.heartbeat.on('heartbeat.error', (err) => {
this.logger.error(`Heartbeat error: ${err.message}`);
this.handleError(err);
});
this.logger.debug('Starting heartbeat');
this.heartbeat.run((err) => {
if (err) {
this.logger.error(`Failed to start heartbeat: ${err.message}`);
cb(err);
}
else {
this.logger.debug('Heartbeat started successfully');
cb();
}
});
}
catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
cb(err);
}
};
shutdownHeartbeat = (cb) => {
if (this.heartbeat) {
this.logger.debug('Shutting down heartbeat');
this.heartbeat.shutdown((err) => {
if (err) {
this.logger.warn(`Error during heartbeat shutdown: ${err.message}`);
}
this.logger.debug('Heartbeat shut down');
this.heartbeat = null;
cb();
});
}
else {
this.logger.debug('No heartbeat to shut down');
cb();
}
};
runMessageHandlers = (cb) => {
this.logger.debug('Starting message handlers');
this.messageHandlerRunner.run((err) => {
if (err) {
this.logger.error(`Failed to start message handlers: ${err.message}`);
cb(err);
}
else {
this.logger.debug('Message handlers started successfully');
cb();
}
});
};
shutdownMessageHandlers = (cb) => {
this.logger.debug('Shutting down message handlers');
this.messageHandlerRunner.shutdown((err) => {
if (err) {
this.logger.warn(`Error during message handlers shutdown: ${err.message}`);
}
this.logger.debug('Message handlers shut down');
cb();
});
};
goingUp() {
return super.goingUp().concat([
(cb) => {
this.logger.debug(`Emitting consumer.goingUp event for consumer ${this.id}`);
this.emit('consumer.goingUp', this.id);
cb();
},
this.initRedisClient,
this.setUpHeartbeat,
this.runMessageHandlers,
]);
}
goingDown() {
this.emit('consumer.goingDown', this.id);
return [
this.shutdownMessageHandlers,
this.shutdownHeartbeat,
this.releaseRedisClient,
].concat(super.goingDown());
}
finalizeUp() {
super.finalizeUp();
this.emit('consumer.up', this.id);
}
finalizeDown() {
super.finalizeDown();
this.emit('consumer.down', this.id);
}
handleError(err) {
if (!this.isOperational())
return;
this.logger.error(`Consumer error: ${err.message}`, err);
this.logger.debug(`Emitting consumer.error event for consumer ${this.id}`);
this.emit('consumer.error', err, this.id);
super.handleError(err);
}
consume(queue, messageHandler, cb) {
return async.withOptionalCallback(cb, (callback) => {
this.logger.info(`Setting up consumption for queue: ${typeof queue === 'string' ? queue : JSON.stringify(queue)}`);
const parsedQueueParams = _parseQueueExtendedParams(queue);
if (parsedQueueParams instanceof Error) {
this.logger.error(`Failed to parse queue parameters: ${parsedQueueParams.message}`);
callback(parsedQueueParams);
}
else {
this.logger.debug(`Adding message handler for queue: ${JSON.stringify(parsedQueueParams)}`);
this.messageHandlerRunner.addMessageHandler(parsedQueueParams, messageHandler, (err) => {
if (err) {
this.logger.error(`Failed to add message handler: ${err.message}`);
return callback(err);
}
this.logger.info(`Successfully set up consumption for queue: ${parsedQueueParams.queueParams.name}@${parsedQueueParams.queueParams.ns}${parsedQueueParams.groupId ? `, group: ${parsedQueueParams.groupId}` : ''}`);
callback();
});
}
});
}
cancel(queue, cb) {
return async.withOptionalCallback(cb, (callback) => {
this.logger.info(`Canceling consumption for queue: ${typeof queue === 'string' ? queue : JSON.stringify(queue)}`);
const parsedQueueParams = _parseQueueExtendedParams(queue);
if (parsedQueueParams instanceof Error) {
this.logger.error(`Failed to parse queue parameters: ${parsedQueueParams.message}`);
callback(parsedQueueParams);
return;
}
this.logger.debug(`Removing message handler for queue: ${JSON.stringify(parsedQueueParams)}`);
this.messageHandlerRunner.removeMessageHandler(parsedQueueParams, (err) => {
if (err) {
this.logger.error(`Failed to remove message handler: ${err.message}`);
callback(err);
}
else {
this.logger.info(`Successfully canceled consumption for queue: ${parsedQueueParams.queueParams.name}@${parsedQueueParams.queueParams.ns}${parsedQueueParams.groupId ? `, group: ${parsedQueueParams.groupId}` : ''}`);
callback();
}
});
});
}
getQueues() {
this.logger.debug('Getting list of queues being consumed');
const queues = this.messageHandlerRunner.getQueues();
this.logger.debug(`Consumer is handling ${queues.length} queues: ${JSON.stringify(queues)}`);
return queues;
}
getQueuesWithStatus() {
this.logger.debug('Getting list of queues being consumed with consumption status...');
const queues = this.messageHandlerRunner.getQueueWithStatus();
this.logger.debug(`Consumer is handling ${queues.length} queues: ${JSON.stringify(queues)}`);
return queues;
}
static setDefaultOptions(options) {
this.defaultOptions = _parseConsumerOptions(options, Consumer.defaultOptions);
}
static getDefaultOptions() {
return {
...this.defaultOptions,
};
}
}
//# sourceMappingURL=consumer.js.map