@message-queue-toolkit/amqp
Version:
AMQP adapter for message-queue-toolkit
218 lines • 10.1 kB
JavaScript
import { HandlerContainer, isMessageError, parseMessage } from '@message-queue-toolkit/core';
import { AbstractAmqpService } from "./AbstractAmqpService.js";
import { readAmqpMessage } from "./amqpMessageReader.js";
const ABORT_EARLY_EITHER = { error: 'abort' };
const DEFAULT_MAX_RETRY_DURATION = 4 * 24 * 60 * 60;
export class AbstractAmqpConsumer extends AbstractAmqpService {
transactionObservabilityManager;
errorResolver;
executionContext;
deadLetterQueueOptions;
maxRetryDuration;
_messageSchemaContainer;
handlerContainer;
queueName;
constructor(dependencies, options, executionContext) {
super(dependencies, options);
this.transactionObservabilityManager = dependencies.transactionObservabilityManager;
this.errorResolver = dependencies.consumerErrorResolver;
this.deadLetterQueueOptions = options.deadLetterQueue;
this.maxRetryDuration = options.maxRetryDuration ?? DEFAULT_MAX_RETRY_DURATION;
this.queueName = options.locatorConfig
? options.locatorConfig.queueName
: // biome-ignore lint/style/noNonNullAssertion: <explanation>
options.creationConfig.queueName;
this._messageSchemaContainer = this.resolveConsumerMessageSchemaContainer(options);
this.handlerContainer = new HandlerContainer({
messageTypeField: this.messageTypeField,
messageHandlers: options.handlers,
});
this.executionContext = executionContext;
}
async start() {
await this.init();
if (!this.channel)
throw new Error('Channel is not set');
}
async init() {
if (this.deadLetterQueueOptions) {
// TODO: https://www.cloudamqp.com/blog/when-and-how-to-use-the-rabbitmq-dead-letter-exchange.html
throw new Error('deadLetterQueue parameter is not currently supported by the Amqp adapter');
}
await super.init();
}
async receiveNewConnection(connection) {
await super.receiveNewConnection(connection);
await this.consume();
}
async consume() {
await this.channel.consume(this.queueName, (message) => {
if (message === null) {
return;
}
const messageProcessingStartTimestamp = Date.now();
const deserializedMessage = this.deserializeMessage(message);
if (deserializedMessage.error === 'abort') {
this.channel.nack(message, false, false);
const messageId = this.tryToExtractId(message);
this.handleMessageProcessed({
message: null,
processingResult: { status: 'error', errorReason: 'invalidMessage' },
messageProcessingStartTimestamp,
queueName: this.queueName,
messageId: messageId.result,
});
return;
}
const { originalMessage, parsedMessage } = deserializedMessage.result;
// @ts-ignore
const messageType = parsedMessage[this.messageTypeField];
const transactionSpanId = `queue_${this.queueName}:${
// @ts-ignore
parsedMessage[this.messageTypeField]}`;
// @ts-ignore
const uniqueTransactionKey = parsedMessage[this.messageIdField];
this.transactionObservabilityManager?.start(transactionSpanId, uniqueTransactionKey);
if (this.logMessages) {
const resolvedLogMessage = this.resolveMessageLog(parsedMessage, messageType);
this.logMessage(resolvedLogMessage);
}
this.internalProcessMessage(parsedMessage, messageType)
.then((result) => {
if (result.result === 'success') {
this.channel.ack(message);
this.handleMessageProcessed({
message: parsedMessage,
processingResult: { status: 'consumed' },
messageProcessingStartTimestamp,
queueName: this.queueName,
});
return;
}
// requeue the message if maxRetryDuration is not exceeded, else ack it to avoid infinite loop
if (this.shouldBeRetried(originalMessage, this.maxRetryDuration)) {
// TODO: Add retry delay + republish message updating internal properties
this.channel.nack(message, false, true);
this.handleMessageProcessed({
message: parsedMessage,
processingResult: { status: 'retryLater' },
messageProcessingStartTimestamp,
queueName: this.queueName,
});
}
else {
// ToDo move message to DLQ once it is implemented
this.channel.ack(message);
this.handleMessageProcessed({
message: parsedMessage,
processingResult: { status: 'error', errorReason: 'retryLaterExceeded' },
messageProcessingStartTimestamp,
queueName: this.queueName,
});
}
})
.catch((err) => {
// ToDo we need sanity check to stop trying at some point, perhaps some kind of Redis counter
// If we fail due to unknown reason, let's retry
this.channel.nack(message, false, true);
this.handleMessageProcessed({
message: parsedMessage,
processingResult: { status: 'retryLater' },
messageProcessingStartTimestamp,
queueName: this.queueName,
});
this.handleError(err);
})
.finally(() => {
this.transactionObservabilityManager?.stop(uniqueTransactionKey);
});
});
}
async internalProcessMessage(message, messageType) {
const preHandlerOutput = await this.processPrehandlers(message, messageType);
const barrierResult = await this.preHandlerBarrier(message, messageType, preHandlerOutput);
if (barrierResult.isPassing) {
return this.processMessage(message, messageType, {
preHandlerOutput,
barrierOutput: barrierResult.output,
});
}
return { error: 'retryLater' };
}
processMessage(message, messageType,
// biome-ignore lint/suspicious/noExplicitAny: We neither know, nor care about the type here
preHandlingOutputs) {
const handler = this.handlerContainer.resolveHandler(messageType);
return handler.handler(message, this.executionContext, preHandlingOutputs);
}
processPrehandlers(message, messageType) {
const handlerConfig = this.handlerContainer.resolveHandler(messageType);
return this.processPrehandlersInternal(handlerConfig.preHandlers, message);
}
preHandlerBarrier(message, messageType, preHandlerOutput) {
const handler = this.handlerContainer.resolveHandler(messageType);
return this.preHandlerBarrierInternal(handler.preHandlerBarrier, message, this.executionContext, preHandlerOutput);
}
resolveSchema(message) {
return this._messageSchemaContainer.resolveSchema(message);
}
resolveMessageLog(message, messageType) {
const handler = this.handlerContainer.resolveHandler(messageType);
return handler.messageLogFormatter(message);
}
// eslint-disable-next-line max-params
resolveNextFunction(preHandlers, message, index, preHandlerOutput, resolve, reject) {
return this.resolveNextPreHandlerFunctionInternal(preHandlers, this.executionContext, message, index, preHandlerOutput, resolve, reject);
}
deserializeMessage(message) {
const resolveMessageResult = this.resolveMessage(message);
if (isMessageError(resolveMessageResult.error)) {
this.handleError(resolveMessageResult.error);
return ABORT_EARLY_EITHER;
}
// Empty content for whatever reason
if (!resolveMessageResult.result || !resolveMessageResult.result.body) {
return ABORT_EARLY_EITHER;
}
const resolveSchemaResult = this.resolveSchema(resolveMessageResult.result.body);
if (resolveSchemaResult.error) {
this.handleError(resolveSchemaResult.error);
return ABORT_EARLY_EITHER;
}
const deserializationResult = parseMessage(resolveMessageResult.result.body, resolveSchemaResult.result, this.errorResolver);
if (isMessageError(deserializationResult.error)) {
this.handleError(deserializationResult.error);
return ABORT_EARLY_EITHER;
}
// Empty content for whatever reason
if (!deserializationResult.result) {
return ABORT_EARLY_EITHER;
}
return {
result: deserializationResult.result,
};
}
tryToExtractId(message) {
const resolveMessageResult = this.resolveMessage(message);
if (isMessageError(resolveMessageResult.error)) {
this.handleError(resolveMessageResult.error);
return ABORT_EARLY_EITHER;
}
const resolvedMessage = resolveMessageResult.result;
// Empty content for whatever reason
if (!resolvedMessage || !resolvedMessage.body)
return ABORT_EARLY_EITHER;
// @ts-ignore
if (this.messageIdField in resolvedMessage.body) {
return {
// @ts-ignore
result: resolvedMessage.body[this.messageIdField],
};
}
return ABORT_EARLY_EITHER;
}
resolveMessage(message) {
return readAmqpMessage(message, this.errorResolver);
}
}
//# sourceMappingURL=AbstractAmqpConsumer.js.map