redis-smq
Version:
A simple high-performance Redis message queue for Node.js.
243 lines • 9.56 kB
JavaScript
import { async, CallbackEmptyReplyError, logger, PanicError, Runnable, } from 'redis-smq-common';
import { RedisClient } from '../../common/redis-client/redis-client.js';
import { ELuaScriptName } from '../../common/redis-client/scripts/scripts.js';
import { redisKeys } from '../../common/redis-keys/redis-keys.js';
import { Configuration } from '../../config/index.js';
import { EventBus } from '../event-bus/index.js';
import { _getExchangeQueues } from '../exchange/_/_get-exchange-queues.js';
import { EExchangeType } from '../exchange/index.js';
import { EMessageProperty, EMessagePropertyStatus, } from '../message/index.js';
import { MessageEnvelope } from '../message/message-envelope.js';
import { EQueueProperty, EQueueType } from '../queue/index.js';
import { _scheduleMessage } from './_/_schedule-message.js';
import { ProducerError, ProducerExchangeNoMatchedQueueError, ProducerInstanceNotRunningError, ProducerMessageExchangeRequiredError, ProducerMessagePriorityRequiredError, ProducerPriorityQueuingNotEnabledError, ProducerQueueMissingConsumerGroupsError, ProducerQueueNotFoundError, ProducerUnknownQueueTypeError, } from './errors/index.js';
import { eventBusPublisher } from './event-bus-publisher.js';
import { QueueConsumerGroupsCache } from './queue-consumer-groups-cache.js';
export class Producer extends Runnable {
logger;
redisClient;
eventBus;
queueConsumerGroupsHandler = null;
constructor() {
super();
this.redisClient = new RedisClient();
this.redisClient.on('error', (err) => this.handleError(err));
this.eventBus = new EventBus();
this.eventBus.on('error', (err) => this.handleError(err));
this.logger = logger.getLogger(Configuration.getSetConfig().logger, `producer:${this.id}`);
if (Configuration.getSetConfig().eventBus.enabled) {
eventBusPublisher(this, this.eventBus, this.logger);
}
}
getLogger() {
return this.logger;
}
initQueueConsumerGroupsHandler = (cb) => {
this.queueConsumerGroupsHandler = new QueueConsumerGroupsCache(this, this.redisClient, this.eventBus, this.logger);
this.queueConsumerGroupsHandler.run((err) => cb(err));
};
shutDownQueueConsumerGroupsHandler = (cb) => {
if (this.queueConsumerGroupsHandler) {
this.queueConsumerGroupsHandler.shutdown(() => {
this.queueConsumerGroupsHandler = null;
cb();
});
}
else
cb();
};
initRedisClient = (cb) => this.redisClient.getSetInstance((err, client) => {
if (err)
cb(err);
else if (!client)
cb(new CallbackEmptyReplyError());
else {
client.on('error', (err) => this.handleError(err));
cb();
}
});
goingUp() {
return super.goingUp().concat([
this.redisClient.init,
this.eventBus.init,
(cb) => {
this.emit('producer.goingUp', this.id);
cb();
},
this.initRedisClient,
this.initQueueConsumerGroupsHandler,
]);
}
up(cb) {
super.up(() => {
this.emit('producer.up', this.id);
cb(null, true);
});
}
goingDown() {
this.emit('producer.goingDown', this.id);
return [
this.shutDownQueueConsumerGroupsHandler,
this.redisClient.shutdown,
].concat(super.goingDown());
}
down(cb) {
super.down(() => {
this.emit('producer.down', this.id);
setTimeout(() => this.eventBus.shutdown(() => cb(null, true)), 1000);
});
}
getQueueConsumerGroupsHandler() {
if (!this.queueConsumerGroupsHandler)
throw new PanicError(`Expected an instance of QueueConsumerGroupsHandler`);
return this.queueConsumerGroupsHandler;
}
enqueue(redisClient, message, cb) {
const messageState = message.getMessageState();
messageState.setPublishedAt(Date.now());
const messageId = message.getId();
const keys = redisKeys.getQueueKeys(message.getDestinationQueue(), message.getConsumerGroupId());
const { keyMessage } = redisKeys.getMessageKeys(messageId);
const scriptArgs = [
EQueueProperty.QUEUE_TYPE,
EQueueProperty.MESSAGES_COUNT,
EQueueType.PRIORITY_QUEUE,
EQueueType.LIFO_QUEUE,
EQueueType.FIFO_QUEUE,
message.producibleMessage.getPriority() ?? '',
messageId,
EMessageProperty.STATUS,
EMessagePropertyStatus.PENDING,
EMessageProperty.STATE,
JSON.stringify(messageState),
EMessageProperty.MESSAGE,
JSON.stringify(message.toJSON()),
];
redisClient.runScript(ELuaScriptName.PUBLISH_MESSAGE, [
keys.keyQueueProperties,
keys.keyQueuePriorityPending,
keys.keyQueuePending,
keys.keyQueueMessages,
keyMessage,
], scriptArgs, (err, reply) => {
if (err)
return cb(err);
switch (reply) {
case 'OK':
return cb();
case 'QUEUE_NOT_FOUND':
return cb(new ProducerQueueNotFoundError());
case 'MESSAGE_PRIORITY_REQUIRED':
return cb(new ProducerMessagePriorityRequiredError());
case 'PRIORITY_QUEUING_NOT_ENABLED':
return cb(new ProducerPriorityQueuingNotEnabledError());
case 'UNKNOWN_QUEUE_TYPE':
return cb(new ProducerUnknownQueueTypeError());
default:
return cb(new ProducerError());
}
});
}
produceMessageItem(redisClient, message, queue, cb) {
const messageId = message
.setDestinationQueue(queue)
.getMessageState()
.getId();
const handleResult = (err) => {
if (err) {
cb(err);
}
else {
const action = message.isSchedulable() ? 'scheduled' : 'published';
this.logger.info(`Message (ID ${messageId}) has been ${action}.`);
if (!message.isSchedulable()) {
this.emit('producer.messagePublished', messageId, { queueParams: queue, groupId: message.getConsumerGroupId() }, this.id);
}
cb(null, messageId);
}
};
if (message.isSchedulable()) {
_scheduleMessage(redisClient, message, handleResult);
}
else {
this.enqueue(redisClient, message, handleResult);
}
}
produceMessage(redisClient, message, queue, cb) {
const { exists, consumerGroups } = this.getQueueConsumerGroupsHandler().getConsumerGroups(queue);
if (exists) {
if (!consumerGroups.length) {
cb(new ProducerQueueMissingConsumerGroupsError());
}
const ids = [];
async.eachOf(consumerGroups, (group, _, done) => {
const msg = new MessageEnvelope(message).setConsumerGroupId(group);
this.produceMessageItem(redisClient, msg, queue, (err, reply) => {
if (err)
done(err);
else {
ids.push(String(reply));
done();
}
});
}, (err) => {
if (err)
cb(err);
else
cb(null, ids);
});
}
else {
const msg = new MessageEnvelope(message);
this.produceMessageItem(redisClient, msg, queue, (err, reply) => {
if (err)
cb(err);
else
cb(null, [String(reply)]);
});
}
}
produce(msg, cb) {
if (!this.isUp()) {
return cb(new ProducerInstanceNotRunningError());
}
const redisClient = this.redisClient.getInstance();
if (redisClient instanceof Error) {
return cb(redisClient);
}
const exchangeParams = msg.getExchange();
if (!exchangeParams) {
return cb(new ProducerMessageExchangeRequiredError());
}
if (exchangeParams.type === EExchangeType.DIRECT) {
const queue = exchangeParams.params;
return this.produceMessage(redisClient, msg, queue, cb);
}
_getExchangeQueues(redisClient, exchangeParams, (err, queues) => {
if (err) {
return cb(err);
}
if (!queues?.length) {
return cb(new ProducerExchangeNoMatchedQueueError());
}
const messages = [];
async.eachOf(queues, (queue, index, done) => {
this.produceMessage(redisClient, msg, queue, (err, reply) => {
if (err) {
return done(err);
}
if (reply) {
messages.push(...reply);
}
done();
});
}, (err) => {
if (err) {
return cb(err);
}
cb(null, messages);
});
});
}
}
//# sourceMappingURL=producer.js.map