rocketmq-client-nodejs
Version:
RocketMQ Node.js Client
399 lines (356 loc) • 14.9 kB
text/typescript
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { randomUUID } from 'node:crypto';
import { Code } from '../../proto/apache/rocketmq/v2/definition_pb';
import { MessageView } from '../message';
import { MessageQueue } from '../route';
import { TooManyRequestsException } from '../exception';
import { ConsumeResult, ConsumeResultSuspend } from './ConsumeResult';
import { FilterExpression } from './FilterExpression';
import type { PushConsumer } from './PushConsumer';
const ACK_MESSAGE_FAILURE_BACKOFF_DELAY = 1000;
const CHANGE_INVISIBLE_DURATION_FAILURE_BACKOFF_DELAY = 1000;
const FORWARD_MESSAGE_TO_DLQ_FAILURE_BACKOFF_DELAY = 1000;
const RECEIVING_FLOW_CONTROL_BACKOFF_DELAY = 20;
const RECEIVING_FAILURE_BACKOFF_DELAY = 1000;
const RECEIVING_BACKOFF_DELAY_WHEN_CACHE_IS_FULL = 1000;
export class ProcessQueue {
readonly #consumer: PushConsumer;
#dropped = false;
readonly #mq: MessageQueue;
readonly #filterExpression: FilterExpression;
readonly #cachedMessages: MessageView[] = [];
#cachedMessagesBytes = 0;
#activityTime = Date.now();
#cacheFullTime = 0;
#aborted = false;
constructor(consumer: PushConsumer, mq: MessageQueue, filterExpression: FilterExpression) {
this.#consumer = consumer;
this.#mq = mq;
this.#filterExpression = filterExpression;
}
getMessageQueue(): MessageQueue {
return this.#mq;
}
drop(): void {
this.#dropped = true;
}
expired(): boolean {
const longPollingTimeout = this.#consumer.getPushConsumerSettings().getLongPollingTimeout();
const requestTimeout = this.#consumer.requestTimeoutValue;
const maxIdleDuration = (longPollingTimeout + requestTimeout) * 3;
const idleDuration = Date.now() - this.#activityTime;
if (idleDuration < maxIdleDuration) {
return false;
}
const afterCacheFullDuration = Date.now() - this.#cacheFullTime;
if (afterCacheFullDuration < maxIdleDuration) {
return false;
}
return true;
}
cacheMessages(messageList: MessageView[]): void {
if (!messageList || messageList.length === 0) {
return;
}
for (const messageView of messageList) {
this.#cachedMessages.push(messageView);
this.#cachedMessagesBytes += messageView.body.length;
}
}
#getReceptionBatchSize(): number {
const bufferSize = Math.max(
this.#consumer.cacheMessageCountThresholdPerQueue() - this.cachedMessagesCount(),
1,
);
return Math.min(bufferSize, this.#consumer.getPushConsumerSettings().getReceiveBatchSize());
}
fetchMessageImmediately(): void {
(this.#consumer as any).logger?.info('ProcessQueue.fetchMessageImmediately called, mq=%s@%s@%d',
this.#mq.topic.name, this.#mq.broker.name, this.#mq.queueId);
this.#receiveMessageImmediately();
}
onReceiveMessageException(t: Error, attemptId?: string): void {
const delay = t instanceof TooManyRequestsException
? RECEIVING_FLOW_CONTROL_BACKOFF_DELAY
: RECEIVING_FAILURE_BACKOFF_DELAY;
this.#receiveMessageLater(delay, attemptId);
}
#receiveMessageLater(delay: number, attemptId?: string): void {
if (this.#aborted) return;
setTimeout(() => {
if (this.#aborted) return;
this.#receiveMessage(attemptId);
}, delay);
}
#generateAttemptId(): string {
return randomUUID();
}
receiveMessage(attemptId?: string): void {
this.#receiveMessage(attemptId);
}
#receiveMessage(attemptId?: string): void {
if (this.#dropped) {
return;
}
if (this.#isCacheFull()) {
this.#receiveMessageLater(RECEIVING_BACKOFF_DELAY_WHEN_CACHE_IS_FULL, attemptId);
return;
}
this.#receiveMessageImmediately(attemptId);
}
#receiveMessageImmediately(attemptId?: string): void {
if (this.#aborted) return;
attemptId = attemptId ?? this.#generateAttemptId();
try {
const batchSize = this.#getReceptionBatchSize();
const longPollingTimeout = this.#consumer.getPushConsumerSettings().getLongPollingTimeout();
const request = this.#consumer.wrapPushReceiveMessageRequest(
batchSize, this.#mq, this.#filterExpression, longPollingTimeout, attemptId,
);
this.#activityTime = Date.now();
(this.#consumer as any).logger?.debug?.('Start to receive message, mq=%s@%s@%d, batchSize=%d, attemptId=%s',
this.#mq.topic.name, this.#mq.broker.name, this.#mq.queueId, batchSize, attemptId);
this.#consumer.receiveMessage(request, this.#mq, longPollingTimeout)
.then(messages => {
if (messages.length > 0) {
(this.#consumer as any).logger?.debug?.('Received messages, count=%d, mq=%s@%s@%d',
messages.length, this.#mq.topic.name, this.#mq.broker.name, this.#mq.queueId);
}
this.#onReceiveMessageResult(messages);
})
.catch(err => {
(this.#consumer as any).logger?.error('Failed to receive message, mq=%s@%s@%d, error=%s',
this.#mq.topic.name, this.#mq.broker.name, this.#mq.queueId, err);
this.onReceiveMessageException(err, attemptId);
});
} catch (err) {
(this.#consumer as any).logger?.error('Exception in receiveMessage, mq=%s@%s@%d, error=%s',
this.#mq.topic.name, this.#mq.broker.name, this.#mq.queueId, err);
this.onReceiveMessageException(err as Error, attemptId);
}
}
#onReceiveMessageResult(messages: MessageView[]): void {
if (messages.length > 0) {
this.cacheMessages(messages);
this.#consumer.getConsumeService().consume(this, messages);
}
this.#receiveMessage();
}
#isCacheFull(): boolean {
const cacheMessageCountThreshold = this.#consumer.cacheMessageCountThresholdPerQueue();
const actualCount = this.cachedMessagesCount();
if (cacheMessageCountThreshold <= actualCount) {
this.#cacheFullTime = Date.now();
return true;
}
const cacheMessageBytesThreshold = this.#consumer.cacheMessageBytesThresholdPerQueue();
if (cacheMessageBytesThreshold <= this.#cachedMessagesBytes) {
this.#cacheFullTime = Date.now();
return true;
}
return false;
}
eraseMessage(messageView: MessageView, consumeResult: ConsumeResult): void {
if (!messageView) {
return;
}
consumeResult = this.#convertSuspendResultIfNeeded(consumeResult);
this.#statsConsumptionResult(consumeResult);
const task = consumeResult === ConsumeResult.SUCCESS
? this.#ackMessage(messageView)
: this.#nackMessage(messageView);
task.finally(() => {
this.#evictCache(messageView);
});
}
async #ackMessage(messageView: MessageView, attempt = 1): Promise<void> {
try {
const endpoints = messageView.endpoints;
const response = await this.#consumer.ackMessageViaRpc(
endpoints,
this.#consumer.wrapAckMessageRequest(messageView),
this.#consumer.requestTimeoutValue,
);
const status = response.getStatus()?.toObject();
if (status?.code === Code.INVALID_RECEIPT_HANDLE) {
return; // Forgive to retry
}
if (status?.code !== Code.OK) {
await this.#ackMessageLater(messageView, attempt + 1);
}
} catch {
await this.#ackMessageLater(messageView, attempt + 1);
}
}
async #ackMessageLater(messageView: MessageView, attempt: number): Promise<void> {
if (this.#aborted) return;
await new Promise<void>(resolve => setTimeout(resolve, ACK_MESSAGE_FAILURE_BACKOFF_DELAY));
if (this.#aborted) return;
await this.#ackMessage(messageView, attempt);
}
async #nackMessage(messageView: MessageView): Promise<void> {
const retryPolicy = this.#consumer.getRetryPolicy();
const delay = retryPolicy?.getNextAttemptDelay(messageView.deliveryAttempt ?? 1) ?? 0;
await this.#changeInvisibleDuration(messageView, delay);
}
async #changeInvisibleDuration(messageView: MessageView, duration: number, attempt = 1): Promise<void> {
try {
const endpoints = messageView.endpoints;
const response = await this.#consumer.changeInvisibleDurationViaRpc(
endpoints,
this.#consumer.wrapChangeInvisibleDurationRequest(messageView, duration),
this.#consumer.requestTimeoutValue,
);
const status = response.getStatus()?.toObject();
if (status?.code === Code.INVALID_RECEIPT_HANDLE) {
return; // Forgive to retry
}
if (status?.code !== Code.OK) {
await this.#changeInvisibleDurationLater(messageView, duration, attempt + 1);
}
} catch {
await this.#changeInvisibleDurationLater(messageView, duration, attempt + 1);
}
}
async #changeInvisibleDurationLater(messageView: MessageView, duration: number, attempt: number): Promise<void> {
if (this.#aborted) return;
await new Promise<void>(resolve => setTimeout(resolve, CHANGE_INVISIBLE_DURATION_FAILURE_BACKOFF_DELAY));
if (this.#aborted) return;
await this.#changeInvisibleDuration(messageView, duration, attempt);
}
async eraseFifoMessage(messageView: MessageView, consumeResult: ConsumeResult): Promise<void> {
if (!messageView) {
return;
}
consumeResult = this.#convertSuspendResultIfNeeded(consumeResult);
this.#statsConsumptionResult(consumeResult);
const retryPolicy = this.#consumer.getRetryPolicy();
const maxAttempts = retryPolicy?.getMaxAttempts() ?? 1;
const attempt = messageView.deliveryAttempt ?? 1;
const messageId = messageView.messageId;
const clientId = (this.#consumer as any).clientId;
if (consumeResult === ConsumeResult.FAILURE && attempt < maxAttempts) {
const nextAttemptDelay = retryPolicy?.getNextAttemptDelay(attempt) ?? 0;
// Increment delivery attempt before redelivering
messageView.incrementAndGetDeliveryAttempt();
(this.#consumer as any).logger?.debug(
'Prepare to redeliver the fifo message because of the consumption failure, maxAttempts=%d, '
+ 'attempt=%d, mq=%s, messageId=%s, nextAttemptDelay=%d, clientId=%s',
maxAttempts, messageView.deliveryAttempt, this.#mq, messageId, nextAttemptDelay, clientId,
);
// Redeliver the fifo message
const result = await this.#consumer.getConsumeService().consumeMessage(messageView, nextAttemptDelay);
await this.eraseFifoMessage(messageView, result);
return;
}
if (consumeResult === ConsumeResult.SUCCESS) {
await this.#ackMessage(messageView);
} else if (consumeResult instanceof ConsumeResultSuspend) {
const suspendTimeMs = consumeResult.suspendTimeMs;
(this.#consumer as any).logger?.info(
'Suspend consumption, consumerGroup=%s, topic=%s, liteTopic=%s, messageId=%s, result=%s',
this.#consumer.getConsumerGroup(), messageView.topic, messageView.liteTopic ?? '',
messageId, consumeResult,
);
await this.#changeInvisibleDuration(messageView, suspendTimeMs);
} else {
(this.#consumer as any).logger?.info(
'Failed to consume fifo message finally, run out of attempt times, maxAttempts=%d, attempt=%d, '
+ 'mq=%s, messageId=%s, clientId=%s',
maxAttempts, attempt, this.#mq, messageId, clientId,
);
await this.#forwardToDeadLetterQueue(messageView);
}
this.#evictCache(messageView);
}
async #forwardToDeadLetterQueue(messageView: MessageView, attempt = 1): Promise<void> {
try {
const endpoints = messageView.endpoints;
const response = await this.#consumer.forwardMessageToDeadLetterQueueViaRpc(
endpoints,
this.#consumer.wrapForwardMessageToDeadLetterQueueRequest(messageView),
this.#consumer.requestTimeoutValue,
);
const status = response.getStatus();
if (!status) {
throw new Error('Missing status in forward to dead letter queue response');
}
const statusObj = status.toObject();
if (statusObj.code !== Code.OK) {
await this.#forwardToDeadLetterQueueLater(messageView, attempt + 1);
}
} catch (err) {
if ((err as Error).message === 'Missing status in forward to dead letter queue response') {
throw err; // Re-throw critical error
}
await this.#forwardToDeadLetterQueueLater(messageView, attempt + 1);
}
}
async #forwardToDeadLetterQueueLater(messageView: MessageView, attempt: number): Promise<void> {
if (this.#aborted) return;
await new Promise<void>(resolve => setTimeout(resolve, FORWARD_MESSAGE_TO_DLQ_FAILURE_BACKOFF_DELAY));
if (this.#aborted) return;
await this.#forwardToDeadLetterQueue(messageView, attempt);
}
discardMessage(messageView: MessageView): void {
this.#nackMessage(messageView).finally(() => {
this.#evictCache(messageView);
});
}
discardFifoMessage(messageView: MessageView): void {
this.#forwardToDeadLetterQueue(messageView).finally(() => {
this.#evictCache(messageView);
});
}
#evictCache(messageView: MessageView): void {
const index = this.#cachedMessages.indexOf(messageView);
if (index !== -1) {
this.#cachedMessages.splice(index, 1);
this.#cachedMessagesBytes -= messageView.body.length;
}
}
#convertSuspendResultIfNeeded(consumeResult: ConsumeResult): ConsumeResult {
if (consumeResult instanceof ConsumeResultSuspend) {
if (!(this.#consumer as any).isLiteConsumer?.()) {
(this.#consumer as any).logger?.warn(
'Only LitePushConsumer support ConsumeResultSuspend! Convert to ConsumeResult.FAILURE, '
+ 'consumerGroup=%s, consumerType=%s',
this.#consumer.getConsumerGroup(), this.#consumer.constructor.name,
);
return ConsumeResult.FAILURE;
}
}
return consumeResult;
}
#statsConsumptionResult(consumeResult: ConsumeResult): void {
if (consumeResult === ConsumeResult.SUCCESS) {
this.#consumer.incrementConsumptionOkQuantity();
return;
}
this.#consumer.incrementConsumptionErrorQuantity();
}
cachedMessagesCount(): number {
return this.#cachedMessages.length;
}
cachedMessageBytes(): number {
return this.#cachedMessagesBytes;
}
abort(): void {
this.#aborted = true;
}
}