UNPKG

@message-queue-toolkit/amqp

Version:
218 lines 10.1 kB
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