This is an automated email from the ASF dual-hosted git repository.
RongtongJin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-clients.git
The following commit(s) were added to refs/heads/master by this push:
new 42a965ca feat(consumer): implement LitePushConsumer with FIFO and
standard consume services (#1294)
42a965ca is described below
commit 42a965cab34230120112478e4765599a27c17b6e
Author: Drizzle <[email protected]>
AuthorDate: Mon Jul 6 19:45:01 2026 +0800
feat(consumer): implement LitePushConsumer with FIFO and standard consume
services (#1294)
- Add LitePushConsumerImpl extending PushConsumer for lightweight message
consumption
- Implement LiteFifoConsumeService with liteTopic-based grouping and
suspend support
- Implement LiteStandardConsumeService with local retry via eraseFifoMessage
- Refactor ConsumeResult from enum to class-based design with
ConsumeResultSuspend extension
- Optimize FifoConsumeService: add parameter validation, improve Map
grouping logic
- Enhance ProcessQueue: add FIFO message discard/erase methods with local
retry
- Align LitePushConsumerBuilder with Java client: implement
maxCacheMessageCount,
maxCacheMessageSizeInBytes, consumptionThreadCount configurations
- Add unit tests for LiteFifoConsumeService, ConsumeResult, and
LitePushConsumerBuilder
- Update MessageView to support liteTopic field for lite consumer routing
Co-authored-by: drizzle.zk <[email protected]>
---
nodejs/package-lock.json | 4 +-
nodejs/package.json | 2 +-
nodejs/proto/apache/rocketmq/v2/definition.proto | 2 +-
nodejs/src/consumer/ConsumeResult.ts | 65 +++++++-
nodejs/src/consumer/FifoConsumeService.ts | 171 ++++++++++-----------
nodejs/src/consumer/LiteFifoConsumeService.ts | 103 +++++++++++++
nodejs/src/consumer/LitePushConsumer.ts | 32 +++-
nodejs/src/consumer/LitePushConsumerImpl.ts | 36 +++++
nodejs/src/consumer/LiteStandardConsumeService.ts | 73 +++++++++
nodejs/src/consumer/ProcessQueue.ts | 71 ++++++++-
nodejs/src/consumer/PushConsumer.ts | 7 +-
nodejs/src/consumer/index.ts | 2 +
nodejs/src/message/MessageView.ts | 13 +-
nodejs/test/consumer/ConsumeResult.test.ts | 43 ++++++
nodejs/test/consumer/FifoConsumeService.test.ts | 2 +-
.../test/consumer/LiteFifoConsumeService.test.ts | 157 +++++++++++++++++++
.../test/consumer/LitePushConsumerBuilder.test.ts | 23 ++-
nodejs/test/message/LiteMessage.test.ts | 6 +-
nodejs/test/message/PriorityMessage.test.ts | 4 +-
nodejs/test/message/PublishingLiteMessage.test.ts | 2 +-
20 files changed, 694 insertions(+), 124 deletions(-)
diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json
index 8a1e2b2a..2cba0785 100644
--- a/nodejs/package-lock.json
+++ b/nodejs/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rocketmq-client-nodejs",
- "version": "1.0.5",
+ "version": "1.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rocketmq-client-nodejs",
- "version": "1.0.5",
+ "version": "1.0.7",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.9.1",
diff --git a/nodejs/package.json b/nodejs/package.json
index 49917098..eda38358 100644
--- a/nodejs/package.json
+++ b/nodejs/package.json
@@ -1,6 +1,6 @@
{
"name": "rocketmq-client-nodejs",
- "version": "1.0.6",
+ "version": "1.0.7",
"description": "RocketMQ Node.js Client",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/nodejs/proto/apache/rocketmq/v2/definition.proto
b/nodejs/proto/apache/rocketmq/v2/definition.proto
index 516474df..a87dee3b 100644
--- a/nodejs/proto/apache/rocketmq/v2/definition.proto
+++ b/nodejs/proto/apache/rocketmq/v2/definition.proto
@@ -617,4 +617,4 @@ message OffsetOption {
MIN = 1;
MAX = 2;
}
-}
\ No newline at end of file
+}
diff --git a/nodejs/src/consumer/ConsumeResult.ts
b/nodejs/src/consumer/ConsumeResult.ts
index e3d2aafa..9dc70a9e 100644
--- a/nodejs/src/consumer/ConsumeResult.ts
+++ b/nodejs/src/consumer/ConsumeResult.ts
@@ -15,7 +15,66 @@
* limitations under the License.
*/
-export enum ConsumeResult {
- SUCCESS = 'SUCCESS',
- FAILURE = 'FAILURE',
+/**
+ * Consume result for push consumer.
+ *
+ * <p>Designed for push consumer specifically. This is a class-based result
+ * (matching the Java client) so that it can be extended with suspend
results.</p>
+ */
+export class ConsumeResult {
+ /**
+ * Consume message successfully.
+ */
+ static readonly SUCCESS = new ConsumeResult('SUCCESS');
+
+ /**
+ * Failed to consume message.
+ */
+ static readonly FAILURE = new ConsumeResult('FAILURE');
+
+ readonly name: string;
+
+ protected constructor(name: string) {
+ this.name = name;
+ }
+
+ toString(): string {
+ return this.name;
+ }
+}
+
+const MIN_SUSPEND_TIME_MS = 50;
+
+/**
+ * Suspend consume result.
+ *
+ * <p>When returned by the message listener, the consumer will suspend
+ * consumption of the current message (and related messages in FIFO mode)
+ * for the specified duration by changing the invisible duration.</p>
+ */
+export class ConsumeResultSuspend extends ConsumeResult {
+ readonly suspendTimeMs: number;
+
+ private constructor(suspendTimeMs: number) {
+ super('SUSPEND');
+ if (suspendTimeMs < MIN_SUSPEND_TIME_MS) {
+ throw new Error(`suspend time cannot be less than
${MIN_SUSPEND_TIME_MS}ms, got ${suspendTimeMs}ms`);
+ }
+ this.suspendTimeMs = suspendTimeMs;
+ }
+
+ /**
+ * Create a suspend result with the given suspend time in milliseconds.
+ *
+ * @param suspendTimeMs - Suspend time in milliseconds
+ * @return {ConsumeResultSuspend} ConsumeResultSuspend instance
+ * @throws {Error} if suspendTimeMs is less than 50ms
+ */
+ static of(suspendTimeMs: number): ConsumeResultSuspend {
+ return new ConsumeResultSuspend(suspendTimeMs);
+ }
+
+ toString(): string {
+ return `SUSPEND(${this.suspendTimeMs}ms)`;
+ }
}
diff --git a/nodejs/src/consumer/FifoConsumeService.ts
b/nodejs/src/consumer/FifoConsumeService.ts
index 58bdffd2..f7bfae74 100644
--- a/nodejs/src/consumer/FifoConsumeService.ts
+++ b/nodejs/src/consumer/FifoConsumeService.ts
@@ -22,125 +22,110 @@ import type { ProcessQueue } from './ProcessQueue';
import { ILogger, getDefaultLogger } from '../client/Logger';
export class FifoConsumeService extends ConsumeService {
- readonly #enableAccelerator: boolean;
- readonly #groupProcessing = new Map<string /* messageGroup */,
Promise<void>>();
+ readonly #enableFifoConsumeAccelerator: boolean;
readonly #logger: ILogger;
- constructor(clientId: string, messageListener: MessageListener,
enableAccelerator?: boolean) {
+ constructor(clientId: string, messageListener: MessageListener,
enableFifoConsumeAccelerator?: boolean) {
super(clientId, messageListener);
- this.#enableAccelerator = enableAccelerator ?? false;
+ this.#enableFifoConsumeAccelerator = enableFifoConsumeAccelerator ?? false;
this.#logger = getDefaultLogger();
}
consume(pq: ProcessQueue, messageViews: MessageView[]): void {
- // Use iterative consumption when accelerator is disabled or only one
message
- if (!this.#enableAccelerator || messageViews.length <= 1) {
- this.#consumeIteratively(pq, messageViews, 0);
+ if (!pq || !messageViews) {
+ this.#logger.error('[Bug] Invalid arguments for consume, pq=%s,
messageViews=%s, clientId=%s',
+ pq, messageViews, this.clientId);
+ return;
+ }
+ if (!this.#enableFifoConsumeAccelerator || messageViews.length <= 1) {
+ this.consumeIteratively(pq, messageViews, 0).catch(() => {
+ // Error already logged, ignore
+ });
return;
}
- this.#consumeWithAccelerator(pq, messageViews);
- }
-
- /**
- * FIFO consume accelerator mode:
- * - Messages with the same messageGroup are consumed sequentially
- * - Messages with different messageGroups are consumed in parallel
- * - Messages without messageGroup are consumed in parallel
- */
- #consumeWithAccelerator(pq: ProcessQueue, messageViews: MessageView[]): void
{
- // Group messages by messageGroup
- const groupedMessages = new Map<string, MessageView[]>();
- const ungroupedMessages: MessageView[] = [];
+ // Group messages by group key. Default to null-key group for unkeyed
messages.
+ const messageViewsGroupByGroupKey = new Map<string, MessageView[]>();
for (const messageView of messageViews) {
- if (messageView.corrupted) {
- pq.discardFifoMessage(messageView);
- continue;
- }
-
- const messageGroup = messageView.messageGroup || '';
- if (messageGroup) {
- const group = groupedMessages.get(messageGroup) || [];
- group.push(messageView);
- groupedMessages.set(messageGroup, group);
- } else {
- ungroupedMessages.push(messageView);
+ const groupKey = this.getMessageGroupKey(messageView);
+ let group = messageViewsGroupByGroupKey.get(groupKey);
+ if (!group) {
+ group = [];
+ messageViewsGroupByGroupKey.set(groupKey, group);
}
+ group.push(messageView);
}
- // Log parallel consumption info
- const groupCount = groupedMessages.size + (ungroupedMessages.length > 0 ?
1 : 0);
this.#logger.debug?.('FifoConsumeService parallel consume,
messageViewsNum=%d, groupNum=%d',
- messageViews.length, groupCount);
-
- // Process grouped messages (each group sequentially, groups in parallel)
- for (const [ , messages ] of groupedMessages.entries()) {
- this.#processMessageGroup(pq, messages);
- }
+ messageViews.length, messageViewsGroupByGroupKey.size);
- // Process ungrouped messages in parallel
- for (const messageView of ungroupedMessages) {
- this.consumeMessage(messageView)
- .then(result => pq.eraseFifoMessage(messageView, result))
- .catch(() => {
- // Error already logged, continue with next message
- });
+ for (const list of messageViewsGroupByGroupKey.values()) {
+ this.consumeIteratively(pq, list, 0).catch(() => {
+ // Error already logged, ignore
+ });
}
}
/**
- * Process messages within the same group sequentially
+ * Get the group key for the given message view.
+ * Subclasses can override this method to provide different grouping logic.
+ *
+ * @param messageView - The message view
+ * @return {string} The group key, empty string means no grouping
*/
- async #processMessageGroup(pq: ProcessQueue, messages: MessageView[]):
Promise<void> {
- // Check if there's already processing happening for this group
- const messageGroup = messages[0]?.messageGroup || 'NO_GROUP';
- const existingPromise = this.#groupProcessing.get(messageGroup);
- const processTask = (async () => {
- // Wait for previous processing to complete if any
- if (existingPromise) {
- await existingPromise.catch(() => {
- // Ignore previous errors, continue processing
- });
- }
-
- // Process messages in sequence
- for (const messageView of messages) {
- try {
- const result = await this.consumeMessage(messageView);
- await pq.eraseFifoMessage(messageView, result);
- } catch (error) {
- this.#logger.error('Failed to process FIFO message, messageGroup=%s,
messageId=%s, error=%s',
- messageGroup, messageView.messageId, error);
- // Continue with next message even if current fails
- }
- }
- })();
-
- // Store the promise for this group
- this.#groupProcessing.set(messageGroup, processTask);
-
- // Clean up when done
- processTask.finally(() => {
- this.#groupProcessing.delete(messageGroup);
- });
+ protected getMessageGroupKey(messageView: MessageView): string {
+ return messageView.messageGroup || '';
}
- #consumeIteratively(pq: ProcessQueue, messageViews: MessageView[], index:
number): void {
- if (index >= messageViews.length) {
- return;
+ /**
+ * Consume messages iteratively using the provided message view array.
+ * This method handles corrupted messages and continues processing the next
valid message.
+ *
+ * @param pq - The process queue
+ * @param messageViews - The message views to consume
+ * @param startIndex - The index to start consuming from
+ * @return {Promise<void>} Promise that resolves when all messages are
consumed
+ */
+ protected consumeIteratively(pq: ProcessQueue, messageViews: MessageView[],
startIndex: number): Promise<void> {
+ const next = this.getNextValidMessage(pq, messageViews, startIndex);
+ if (!next) {
+ return Promise.resolve();
}
+ const { messageView, nextIndex } = next;
- const messageView = messageViews[index];
+ return this.consumeMessage(messageView)
+ .then(result => pq.eraseFifoMessage(messageView, result))
+ .then(() => this.consumeIteratively(pq, messageViews, nextIndex))
+ .catch(() => this.consumeIteratively(pq, messageViews, nextIndex));
+ }
- if (messageView.corrupted) {
- pq.discardFifoMessage(messageView);
- this.#consumeIteratively(pq, messageViews, index + 1);
- return;
+ /**
+ * Get the next valid message from the message view array.
+ * Skip corrupted messages and return the first valid one.
+ * Returns null if there are no more messages.
+ *
+ * @param pq - The process queue
+ * @param messageViews - The message views
+ * @param startIndex - The index to start searching from
+ * @return {{ messageView: MessageView; nextIndex: number } | null} The next
valid message and the index after it, or null
+ */
+ protected getNextValidMessage(
+ pq: ProcessQueue,
+ messageViews: MessageView[],
+ startIndex: number,
+ ): { messageView: MessageView; nextIndex: number } | null {
+ for (let i = startIndex; i < messageViews.length; i++) {
+ const messageView = messageViews[i];
+ if (messageView.corrupted) {
+ // Discard corrupted message.
+ this.#logger.error('Message is corrupted for FIFO consumption, prepare
to discard it, mq=%s, '
+ + 'messageId=%s, clientId=%s',
+ pq.getMessageQueue(), messageView.messageId, this.clientId);
+ pq.discardFifoMessage(messageView);
+ continue;
+ }
+ return { messageView, nextIndex: i + 1 };
}
-
- this.consumeMessage(messageView)
- .then(result => pq.eraseFifoMessage(messageView, result))
- .then(() => this.#consumeIteratively(pq, messageViews, index + 1))
- .catch(() => this.#consumeIteratively(pq, messageViews, index + 1));
+ return null;
}
}
diff --git a/nodejs/src/consumer/LiteFifoConsumeService.ts
b/nodejs/src/consumer/LiteFifoConsumeService.ts
new file mode 100644
index 00000000..29f39ab3
--- /dev/null
+++ b/nodejs/src/consumer/LiteFifoConsumeService.ts
@@ -0,0 +1,103 @@
+/**
+ * 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 { MessageView } from '../message';
+import { ConsumeResultSuspend } from './ConsumeResult';
+import { FifoConsumeService } from './FifoConsumeService';
+import { MessageListener } from './MessageListener';
+import type { ProcessQueue } from './ProcessQueue';
+import { ILogger, getDefaultLogger } from '../client/Logger';
+
+/**
+ * FIFO consume service for lite push consumer.
+ *
+ * <p>Similar to {@link FifoConsumeService}, but groups messages by lite topic
+ * instead of message group. This ensures messages with the same lite topic are
+ * consumed sequentially, while messages with different lite topics can be
+ * consumed in parallel when the accelerator is enabled.</p>
+ *
+ * <p>When the message listener returns {@link ConsumeResultSuspend}, all
+ * remaining messages with the same lite topic in the current batch are also
+ * suspended, while messages with different lite topics continue to be
consumed.</p>
+ */
+export class LiteFifoConsumeService extends FifoConsumeService {
+ readonly #logger: ILogger;
+
+ constructor(clientId: string, messageListener: MessageListener,
enableFifoConsumeAccelerator?: boolean) {
+ super(clientId, messageListener, enableFifoConsumeAccelerator);
+ this.#logger = getDefaultLogger();
+ }
+
+ /**
+ * Use lite topic as the group key for FIFO consumption.
+ *
+ * @param messageView - The message view
+ * @return {string} The lite topic, empty string if not present
+ */
+ protected getMessageGroupKey(messageView: MessageView): string {
+ return messageView.liteTopic || '';
+ }
+
+ protected consumeIteratively(pq: ProcessQueue, messageViews: MessageView[],
startIndex: number): Promise<void> {
+ if (!pq || !messageViews) {
+ this.#logger.error('[Bug] Invalid arguments for consumeIteratively,
pq=%s, messageViews=%s, clientId=%s',
+ pq, messageViews, this.clientId);
+ return Promise.resolve();
+ }
+
+ const next = this.getNextValidMessage(pq, messageViews, startIndex);
+ if (!next) {
+ return Promise.resolve();
+ }
+ const { messageView, nextIndex } = next;
+
+ return this.consumeMessage(messageView)
+ .then(async result => {
+ await pq.eraseFifoMessage(messageView, result);
+ return result;
+ })
+ .then(async result => {
+ if (result instanceof ConsumeResultSuspend) {
+ const currentLiteTopic = messageView.liteTopic ?? '';
+ const continuation: MessageView[] = [];
+ const suspendTasks: Array<Promise<void>> = [];
+
+ // Suspend all messages with the same liteTopic in this batch.
+ for (let i = nextIndex; i < messageViews.length; i++) {
+ const msg = messageViews[i];
+ if ((msg.liteTopic ?? '') === currentLiteTopic) {
+ suspendTasks.push(pq.eraseFifoMessage(msg, result));
+ } else {
+ continuation.push(msg);
+ }
+ }
+
+ this.#logger.debug?.('LiteFifoConsumeService suspend, liteTopic=%s,
suspendCount=%d, continuationCount=%d, '
+ + 'clientId=%s', currentLiteTopic, suspendTasks.length,
continuation.length, this.clientId);
+
+ await Promise.all(suspendTasks);
+ return this.consumeIteratively(pq, continuation, 0);
+ }
+ return this.consumeIteratively(pq, messageViews, nextIndex);
+ })
+ .catch(err => {
+ this.#logger.error('[Bug] Exception raised in lite FIFO consumption
callback, clientId=%s, error=%s',
+ this.clientId, err);
+ return this.consumeIteratively(pq, messageViews, nextIndex);
+ });
+ }
+}
diff --git a/nodejs/src/consumer/LitePushConsumer.ts
b/nodejs/src/consumer/LitePushConsumer.ts
index 3517150e..a0df4ff1 100644
--- a/nodejs/src/consumer/LitePushConsumer.ts
+++ b/nodejs/src/consumer/LitePushConsumer.ts
@@ -91,6 +91,10 @@ export interface LitePushConsumerOptions extends
BaseClientOptions {
consumerGroup: string;
bindTopic: string;
messageListener: MessageListener;
+ maxCacheMessageCount?: number;
+ maxCacheMessageSizeInBytes?: number;
+ consumptionThreadCount?: number;
+ enableFifoConsumeAccelerator?: boolean;
}
const CONSUMER_GROUP_PATTERN = /^[a-zA-Z0-9_-]+$/;
@@ -178,7 +182,7 @@ export class LitePushConsumerBuilder {
if (maxCacheMessageCount <= 0) {
throw new Error('maxCacheMessageCount should be positive');
}
- // TODO: Will be used when implementing full consumer
+ this.options.maxCacheMessageCount = maxCacheMessageCount;
return this;
}
@@ -193,7 +197,7 @@ export class LitePushConsumerBuilder {
if (maxCacheMessageSizeInBytes <= 0) {
throw new Error('maxCacheMessageSizeInBytes should be positive');
}
- // TODO: Will be used when implementing full consumer
+ this.options.maxCacheMessageSizeInBytes = maxCacheMessageSizeInBytes;
return this;
}
@@ -208,7 +212,25 @@ export class LitePushConsumerBuilder {
if (consumptionThreadCount <= 0) {
throw new Error('consumptionThreadCount should be positive');
}
- // TODO: Will be used when implementing full consumer
+ // Note: Node.js uses a single-threaded event loop model, so this
configuration
+ // has no effect on actual consumption concurrency. It is retained solely
for
+ // API compatibility with the other clients. Consumption concurrency in
Node.js
+ // is naturally managed by the event loop and Promise-based async
scheduling.
+ this.options.consumptionThreadCount = consumptionThreadCount;
+ return this;
+ }
+
+ /**
+ * Set enable fifo consume accelerator.
+ *
+ * <p>If enabled, messages with different messageGroups are consumed in
parallel
+ * while messages within the same messageGroup are consumed sequentially.</p>
+ *
+ * @param enableFifoConsumeAccelerator - Whether to enable FIFO consume
accelerator
+ * @return This builder instance
+ */
+ setEnableFifoConsumeAccelerator(enableFifoConsumeAccelerator: boolean):
LitePushConsumerBuilder {
+ this.options.enableFifoConsumeAccelerator = enableFifoConsumeAccelerator;
return this;
}
@@ -247,6 +269,10 @@ export class LitePushConsumerBuilder {
sessionCredentials: this.options.sessionCredentials,
requestTimeout: this.options.requestTimeout,
logger: this.options.logger,
+ maxCacheMessageCount: this.options.maxCacheMessageCount,
+ maxCacheMessageSizeInBytes: this.options.maxCacheMessageSizeInBytes,
+ consumptionThreadCount: this.options.consumptionThreadCount,
+ enableFifoConsumeAccelerator: this.options.enableFifoConsumeAccelerator,
};
const litePushConsumer = new LitePushConsumerImpl(options);
diff --git a/nodejs/src/consumer/LitePushConsumerImpl.ts
b/nodejs/src/consumer/LitePushConsumerImpl.ts
index fc977323..37440b51 100644
--- a/nodejs/src/consumer/LitePushConsumerImpl.ts
+++ b/nodejs/src/consumer/LitePushConsumerImpl.ts
@@ -24,6 +24,10 @@ import { LitePushConsumer } from './LitePushConsumer';
import { OffsetOption } from './OffsetOption';
import { LiteSubscriptionManager } from './LiteSubscriptionManager';
import { FilterExpression } from './FilterExpression';
+import { LiteFifoConsumeService } from './LiteFifoConsumeService';
+import { LiteStandardConsumeService } from './LiteStandardConsumeService';
+import { ConsumeService } from './ConsumeService';
+import { MessageListener } from './MessageListener';
export interface LitePushConsumerOptions extends PushConsumerOptions {
bindTopic: string;
@@ -50,6 +54,8 @@ export interface LitePushConsumerOptions extends
PushConsumerOptions {
export class LitePushConsumerImpl extends PushConsumer implements
LitePushConsumer {
private readonly liteSubscriptionManager: LiteSubscriptionManager;
private readonly bindTopic: Resource;
+ readonly #messageListener: MessageListener;
+ readonly #enableFifoConsumeAccelerator: boolean;
constructor(options: LitePushConsumerOptions) {
// Create subscription expressions with bind topic
@@ -61,6 +67,9 @@ export class LitePushConsumerImpl extends PushConsumer
implements LitePushConsum
subscriptions,
} as PushConsumerOptions);
+ this.#messageListener = options.messageListener;
+ this.#enableFifoConsumeAccelerator = options.enableFifoConsumeAccelerator
?? false;
+
this.bindTopic = new Resource(options.namespace, options.bindTopic);
const groupResource = new Resource(options.namespace,
options.consumerGroup);
@@ -71,6 +80,27 @@ export class LitePushConsumerImpl extends PushConsumer
implements LitePushConsum
);
}
+ /**
+ * Create the consume service for lite push consumer.
+ *
+ * <p>Lite push consumer uses lite-topic-specific consume services to ensure
+ * correct grouping and retry behavior.</p>
+ */
+ protected createConsumeService(): ConsumeService {
+ if (!this.#messageListener) {
+ throw new Error('messageListener should not be null');
+ }
+ if (this.getPushConsumerSettings().isFifo()) {
+ this.logger.info(
+ 'Create lite FIFO consume service, consumerGroup=%s, clientId=%s,
enableFifoConsumeAccelerator=%s',
+ this.consumerGroup, this.clientId, this.#enableFifoConsumeAccelerator,
+ );
+ return new LiteFifoConsumeService(this.clientId, this.#messageListener,
this.#enableFifoConsumeAccelerator);
+ }
+ this.logger.info('Create lite standard consume service, consumerGroup=%s,
clientId=%s', this.consumerGroup, this.clientId);
+ return new LiteStandardConsumeService(this.clientId,
this.#messageListener);
+ }
+
/**
* Get the client type.
*
@@ -137,6 +167,9 @@ export class LitePushConsumerImpl extends PushConsumer
implements LitePushConsum
async subscribeLite(liteTopic: string, offsetOption: OffsetOption):
Promise<void>;
async subscribeLite(liteTopic: string, offsetOption?: OffsetOption):
Promise<void> {
+ if (!liteTopic || liteTopic.trim().length === 0) {
+ throw new Error('liteTopic should not be blank');
+ }
await this.liteSubscriptionManager.subscribeLite(liteTopic, offsetOption
?? null);
}
@@ -149,6 +182,9 @@ export class LitePushConsumerImpl extends PushConsumer
implements LitePushConsum
* @param liteTopic - The name of the lite topic to unsubscribe from
*/
async unsubscribeLite(liteTopic: string): Promise<void> {
+ if (!liteTopic || liteTopic.trim().length === 0) {
+ throw new Error('liteTopic should not be blank');
+ }
await this.liteSubscriptionManager.unsubscribeLite(liteTopic);
}
diff --git a/nodejs/src/consumer/LiteStandardConsumeService.ts
b/nodejs/src/consumer/LiteStandardConsumeService.ts
new file mode 100644
index 00000000..f3d93ced
--- /dev/null
+++ b/nodejs/src/consumer/LiteStandardConsumeService.ts
@@ -0,0 +1,73 @@
+/**
+ * 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 { MessageView } from '../message';
+import { ConsumeService } from './ConsumeService';
+import { MessageListener } from './MessageListener';
+import type { ProcessQueue } from './ProcessQueue';
+import { ILogger, getDefaultLogger } from '../client/Logger';
+
+/**
+ * Standard consume service for lite push consumer.
+ *
+ * <p>Similar to {@link StandardConsumeService}, but uses
+ * {@link ProcessQueue#eraseFifoMessage} for local retry on failure, avoiding
+ * frequent server requests.</p>
+ */
+export class LiteStandardConsumeService extends ConsumeService {
+ readonly #logger: ILogger;
+
+ constructor(clientId: string, messageListener: MessageListener) {
+ super(clientId, messageListener);
+ this.#logger = getDefaultLogger();
+ }
+
+ consume(pq: ProcessQueue, messageViews: MessageView[]): void {
+ if (!pq || !messageViews) {
+ this.#logger.error('[Bug] Invalid arguments for consume, pq=%s,
messageViews=%s, clientId=%s', pq, messageViews, this.clientId);
+ return;
+ }
+
+ this.#logger.debug?.('LiteStandardConsumeService.consume called,
messageCount=%d, clientId=%s', messageViews.length, this.clientId);
+
+ for (const messageView of messageViews) {
+ if (messageView.corrupted) {
+ this.#logger.error('Message is corrupted for lite standard
consumption, prepare to discard it, mq=%s, '
+ + 'messageId=%s, clientId=%s',
+ pq.getMessageQueue(), messageView.messageId, this.clientId);
+ pq.discardMessage(messageView);
+ continue;
+ }
+
+ this.#logger.debug?.('Consuming lite message, messageId=%s, topic=%s,
liteTopic=%s, clientId=%s',
+ messageView.messageId, messageView.topic, messageView.liteTopic,
this.clientId);
+
+ this.consumeMessage(messageView)
+ .then(result => {
+ this.#logger.debug?.('Lite message consumed successfully,
messageId=%s, result=%s, clientId=%s',
+ messageView.messageId, result, this.clientId);
+ // Use eraseFifoMessage for local retry on failure, avoiding
frequent server requests.
+ pq.eraseFifoMessage(messageView, result);
+ })
+ .catch(err => {
+ // Should never reach here.
+ this.#logger.error('[Bug] Exception raised in lite standard
consumption callback, clientId=%s, error=%s',
+ this.clientId, err);
+ });
+ }
+ }
+}
diff --git a/nodejs/src/consumer/ProcessQueue.ts
b/nodejs/src/consumer/ProcessQueue.ts
index 3d21f802..5fc83387 100644
--- a/nodejs/src/consumer/ProcessQueue.ts
+++ b/nodejs/src/consumer/ProcessQueue.ts
@@ -20,7 +20,7 @@ import { Code } from
'../../proto/apache/rocketmq/v2/definition_pb';
import { MessageView } from '../message';
import { MessageQueue } from '../route';
import { TooManyRequestsException } from '../exception';
-import { ConsumeResult } from './ConsumeResult';
+import { ConsumeResult, ConsumeResultSuspend } from './ConsumeResult';
import { FilterExpression } from './FilterExpression';
import type { PushConsumer } from './PushConsumer';
@@ -73,6 +73,9 @@ export class ProcessQueue {
}
cacheMessages(messageList: MessageView[]): void {
+ if (!messageList || messageList.length === 0) {
+ return;
+ }
for (const messageView of messageList) {
this.#cachedMessages.push(messageView);
this.#cachedMessagesBytes += messageView.body.length;
@@ -187,6 +190,11 @@ export class ProcessQueue {
}
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);
@@ -256,22 +264,51 @@ export class ProcessQueue {
}
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 {
- const task = consumeResult === ConsumeResult.SUCCESS
- ? this.#ackMessage(messageView)
- : this.#forwardToDeadLetterQueue(messageView);
- await task;
- this.#evictCache(messageView);
+ (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> {
@@ -325,6 +362,28 @@ export class ProcessQueue {
}
}
+ #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;
}
diff --git a/nodejs/src/consumer/PushConsumer.ts
b/nodejs/src/consumer/PushConsumer.ts
index 62e751cb..b0afaf70 100644
--- a/nodejs/src/consumer/PushConsumer.ts
+++ b/nodejs/src/consumer/PushConsumer.ts
@@ -103,7 +103,7 @@ export class PushConsumer extends Consumer {
await super.startup();
this.logger.info('Super startup completed, clientId=%s', this.clientId);
try {
- this.#consumeService = this.#createConsumeService();
+ this.#consumeService = this.createConsumeService();
// Start scanning assignments periodically
this.startAssignmentScanning();
this.logger.info('Push consumer started successfully, clientId=%s',
this.clientId);
@@ -181,7 +181,7 @@ export class PushConsumer extends Consumer {
// No-op for push consumer; assignments are queried separately
}
- #createConsumeService(): ConsumeService {
+ protected createConsumeService(): ConsumeService {
if (this.#pushSubscriptionSettings.isFifo()) {
return new FifoConsumeService(this.clientId, this.#messageListener,
this.#enableFifoConsumeAccelerator);
}
@@ -323,9 +323,10 @@ export class PushConsumer extends Consumer {
.setInvisibleDuration(createDuration(invisibleDuration))
.setMessageId(messageView.messageId);
- // For lite consumers, must set liteTopic in request
+ // For lite consumers, must set liteTopic and suspend in request
if (this.isLiteConsumer() && messageView.liteTopic) {
request.setLiteTopic(messageView.liteTopic);
+ request.setSuspend(true);
}
return request;
diff --git a/nodejs/src/consumer/index.ts b/nodejs/src/consumer/index.ts
index eadc3bd7..ff7a14ff 100644
--- a/nodejs/src/consumer/index.ts
+++ b/nodejs/src/consumer/index.ts
@@ -29,6 +29,8 @@ export * from './ConsumeTask';
export * from './ConsumeService';
export * from './StandardConsumeService';
export * from './FifoConsumeService';
+export * from './LiteFifoConsumeService';
+export * from './LiteStandardConsumeService';
export * from './ProcessQueue';
export * from './PushConsumer';
export * from './LitePushConsumer';
diff --git a/nodejs/src/message/MessageView.ts
b/nodejs/src/message/MessageView.ts
index 7c191611..0e17d233 100644
--- a/nodejs/src/message/MessageView.ts
+++ b/nodejs/src/message/MessageView.ts
@@ -36,7 +36,7 @@ export class MessageView {
readonly keys: string[];
readonly bornHost: string;
readonly bornTimestamp?: Date;
- readonly deliveryAttempt?: number;
+ deliveryAttempt?: number;
readonly endpoints: Endpoints;
readonly receiptHandle: string;
readonly offset?: number;
@@ -218,4 +218,15 @@ export class MessageView {
getDeliveryAttempt(): number | undefined {
return this.deliveryAttempt;
}
+
+ /**
+ * Increment the delivery attempt and return the new value.
+ * Used by FIFO retry logic to track redelivery attempts.
+ *
+ * @return the incremented delivery attempt
+ */
+ incrementAndGetDeliveryAttempt(): number {
+ this.deliveryAttempt = (this.deliveryAttempt ?? 0) + 1;
+ return this.deliveryAttempt;
+ }
}
diff --git a/nodejs/test/consumer/ConsumeResult.test.ts
b/nodejs/test/consumer/ConsumeResult.test.ts
new file mode 100644
index 00000000..25db9ed5
--- /dev/null
+++ b/nodejs/test/consumer/ConsumeResult.test.ts
@@ -0,0 +1,43 @@
+/**
+ * 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 { describe, it } from 'node:test';
+import * as assert from 'node:assert';
+import { ConsumeResult, ConsumeResultSuspend } from
'../../src/consumer/ConsumeResult';
+
+describe('ConsumeResult', () => {
+ it('should have SUCCESS and FAILURE singletons', () => {
+ assert.strictEqual(ConsumeResult.SUCCESS.name, 'SUCCESS');
+ assert.strictEqual(ConsumeResult.FAILURE.name, 'FAILURE');
+ assert.strictEqual(ConsumeResult.SUCCESS.toString(), 'SUCCESS');
+ assert.strictEqual(ConsumeResult.FAILURE.toString(), 'FAILURE');
+ assert.strictEqual(ConsumeResult.SUCCESS, ConsumeResult.SUCCESS);
+ });
+
+ it('should create ConsumeResultSuspend with valid suspend time', () => {
+ const suspend = ConsumeResultSuspend.of(100);
+ assert.strictEqual(suspend.name, 'SUSPEND');
+ assert.strictEqual(suspend.suspendTimeMs, 100);
+ assert.strictEqual(suspend.toString(), 'SUSPEND(100ms)');
+ });
+
+ it('should reject suspend time less than 50ms', () => {
+ assert.throws(() => {
+ ConsumeResultSuspend.of(49);
+ }, /suspend time cannot be less than 50ms/);
+ });
+});
diff --git a/nodejs/test/consumer/FifoConsumeService.test.ts
b/nodejs/test/consumer/FifoConsumeService.test.ts
index c60e3025..25b48655 100644
--- a/nodejs/test/consumer/FifoConsumeService.test.ts
+++ b/nodejs/test/consumer/FifoConsumeService.test.ts
@@ -20,7 +20,7 @@
*/
import { describe, it } from 'node:test';
-import assert = require('node:assert');
+import * as assert from 'node:assert';
import { FifoConsumeService } from '../../src/consumer/FifoConsumeService';
import { MessageListener } from '../../src/consumer/MessageListener';
import { ConsumeResult } from '../../src/consumer/ConsumeResult';
diff --git a/nodejs/test/consumer/LiteFifoConsumeService.test.ts
b/nodejs/test/consumer/LiteFifoConsumeService.test.ts
new file mode 100644
index 00000000..0697168e
--- /dev/null
+++ b/nodejs/test/consumer/LiteFifoConsumeService.test.ts
@@ -0,0 +1,157 @@
+/**
+ * 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.
+ */
+
+/**
+ * Test for Lite FIFO Consume Service
+ */
+
+import { describe, it } from 'node:test';
+import * as assert from 'node:assert';
+import { LiteFifoConsumeService } from
'../../src/consumer/LiteFifoConsumeService';
+import { MessageListener } from '../../src/consumer/MessageListener';
+import { ConsumeResult, ConsumeResultSuspend } from
'../../src/consumer/ConsumeResult';
+import type { ProcessQueue } from '../../src/consumer/ProcessQueue';
+import { MessageView } from '../../src/message';
+
+describe('LiteFifoConsumeService', () => {
+ const createMessageListener = (result: ConsumeResult =
ConsumeResult.SUCCESS): MessageListener => {
+ return {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async consume(_messageView: MessageView): Promise<ConsumeResult> {
+ return result;
+ },
+ };
+ };
+
+ const createMockProcessQueue = (): ProcessQueue => {
+ const erased: Array<{ messageView: MessageView; result: ConsumeResult }> =
[];
+ return {
+ eraseFifoMessage: async (messageView: MessageView, result:
ConsumeResult) => {
+ erased.push({ messageView, result });
+ },
+ discardFifoMessage: () => { /* noop */ },
+ getErased: () => erased,
+ } as unknown as ProcessQueue;
+ };
+
+ const createMessageView = (id: string, liteTopic: string): MessageView => {
+ return {
+ messageId: id,
+ liteTopic,
+ corrupted: false,
+ } as unknown as MessageView;
+ };
+
+ it('should support enableFifoConsumeAccelerator option', () => {
+ const messageListener = createMessageListener();
+
+ // Create with accelerator enabled
+ const serviceWithAccelerator = new LiteFifoConsumeService('test-client',
messageListener, true);
+ assert.ok(serviceWithAccelerator, 'Should create service with accelerator
enabled');
+
+ // Create with accelerator disabled (default)
+ const serviceWithoutAccelerator = new
LiteFifoConsumeService('test-client', messageListener, false);
+ assert.ok(serviceWithoutAccelerator, 'Should create service with
accelerator disabled');
+
+ // Create without specifying (default to false)
+ const serviceDefault = new LiteFifoConsumeService('test-client',
messageListener);
+ assert.ok(serviceDefault, 'Should create service with default accelerator
setting');
+ });
+
+ it('should group messages by lite topic', () => {
+ const messageListener = createMessageListener();
+ const service = new LiteFifoConsumeService('test-client', messageListener,
true);
+
+ // Expose protected method for testing
+ const getMessageGroupKey = (service as
any).getMessageGroupKey.bind(service);
+
+ const messageWithLiteTopic = {
+ liteTopic: 'lite-topic-a',
+ messageGroup: 'fifo-group',
+ } as MessageView;
+ assert.strictEqual(getMessageGroupKey(messageWithLiteTopic),
'lite-topic-a',
+ 'Should use lite topic as group key');
+
+ const messageWithoutLiteTopic = {
+ liteTopic: undefined,
+ messageGroup: 'fifo-group',
+ } as MessageView;
+ assert.strictEqual(getMessageGroupKey(messageWithoutLiteTopic), '',
+ 'Should return empty string when lite topic is absent');
+ });
+
+ it('should suspend remaining messages with the same lite topic', async () =>
{
+ const suspendResult = ConsumeResultSuspend.of(100);
+ const messageListener = createMessageListener(suspendResult);
+ const service = new LiteFifoConsumeService('test-client', messageListener,
false);
+ const pq = createMockProcessQueue();
+
+ const msg1 = createMessageView('1', 'topic-a');
+ const msg2 = createMessageView('2', 'topic-a');
+ const msg3 = createMessageView('3', 'topic-b');
+ const msg4 = createMessageView('4', 'topic-a');
+
+ const consumeIteratively = (service as
any).consumeIteratively.bind(service);
+ await consumeIteratively(pq, [ msg1, msg2, msg3, msg4 ], 0);
+
+ const erased = (pq as any).getErased();
+ // msg1 consumed and suspended, msg2/msg4 suspended without consume, msg3
consumed in continuation
+ assert.strictEqual(erased.length, 4, 'All messages should be erased');
+ assert.strictEqual(erased[0].messageView.messageId, '1');
+ assert.strictEqual(erased[0].result, suspendResult);
+ assert.strictEqual(erased[1].messageView.messageId, '2');
+ assert.strictEqual(erased[1].result, suspendResult);
+ assert.strictEqual(erased[2].messageView.messageId, '4');
+ assert.strictEqual(erased[2].result, suspendResult);
+ assert.strictEqual(erased[3].messageView.messageId, '3');
+ assert.strictEqual(erased[3].result, suspendResult);
+ });
+
+ it('should continue consuming messages with different lite topic after
suspend', async () => {
+ const suspendResult = ConsumeResultSuspend.of(100);
+ let callCount = 0;
+ const messageListener: MessageListener = {
+ async consume(messageView: MessageView): Promise<ConsumeResult> {
+ callCount++;
+ if (messageView.liteTopic === 'topic-a') {
+ return suspendResult;
+ }
+ return ConsumeResult.SUCCESS;
+ },
+ };
+ const service = new LiteFifoConsumeService('test-client', messageListener,
false);
+ const pq = createMockProcessQueue();
+
+ const msg1 = createMessageView('1', 'topic-a');
+ const msg2 = createMessageView('2', 'topic-b');
+ const msg3 = createMessageView('3', 'topic-a');
+
+ const consumeIteratively = (service as
any).consumeIteratively.bind(service);
+ await consumeIteratively(pq, [ msg1, msg2, msg3 ], 0);
+
+ const erased = (pq as any).getErased();
+ // msg1 consumed and suspended, msg3 suspended without consume, msg2
consumed in continuation
+ assert.strictEqual(callCount, 2, 'Only msg1 and msg2 should be consumed by
listener');
+ assert.strictEqual(erased.length, 3);
+ assert.strictEqual(erased[0].messageView.messageId, '1');
+ assert.strictEqual(erased[0].result, suspendResult);
+ assert.strictEqual(erased[1].messageView.messageId, '3');
+ assert.strictEqual(erased[1].result, suspendResult);
+ assert.strictEqual(erased[2].messageView.messageId, '2');
+ assert.strictEqual(erased[2].result, ConsumeResult.SUCCESS);
+ });
+});
diff --git a/nodejs/test/consumer/LitePushConsumerBuilder.test.ts
b/nodejs/test/consumer/LitePushConsumerBuilder.test.ts
index ca02efe4..9ee543b8 100644
--- a/nodejs/test/consumer/LitePushConsumerBuilder.test.ts
+++ b/nodejs/test/consumer/LitePushConsumerBuilder.test.ts
@@ -155,6 +155,20 @@ describe('LitePushConsumerBuilder', () => {
});
});
+ describe('setEnableFifoConsumeAccelerator()', () => {
+ it('should accept true value', () => {
+ const builder = new LitePushConsumerBuilder();
+ const result = builder.setEnableFifoConsumeAccelerator(true);
+ assert.strictEqual(result, builder);
+ });
+
+ it('should accept false value', () => {
+ const builder = new LitePushConsumerBuilder();
+ const result = builder.setEnableFifoConsumeAccelerator(false);
+ assert.strictEqual(result, builder);
+ });
+ });
+
describe('build()', () => {
it('should throw error when clientConfiguration not set', async () => {
const builder = new LitePushConsumerBuilder();
@@ -172,10 +186,10 @@ describe('LitePushConsumerBuilder', () => {
builder.setMessageListener(createMockMessageListener());
builder.bindTopic('test-topic');
- // Should reject with "not implemented" error since we haven't
implemented the full consumer yet
+ // Should reject because startup() requires a real RocketMQ server
connection
await assert.rejects(
() => builder.build(),
- /LitePushConsumerImpl not yet implemented/,
+ /Failed to start|Connection|ECONNREFUSED|timeout|Timeout|Error/,
);
});
@@ -206,16 +220,17 @@ describe('LitePushConsumerBuilder', () => {
await assert.rejects(() => builder.build(), /bindTopic has not been set
yet/);
});
- it('should throw not implemented error when all params set', async () => {
+ it('should throw connection error when all params set but no server',
async () => {
const builder = new LitePushConsumerBuilder();
builder.setClientConfiguration(createMockClientConfig());
builder.setConsumerGroup('test-group');
builder.setMessageListener(createMockMessageListener());
builder.bindTopic('test-topic');
+ // Should reject because startup() requires a real RocketMQ server
connection
await assert.rejects(
() => builder.build(),
- /LitePushConsumerImpl not yet implemented/,
+ /Failed to start|Connection|ECONNREFUSED|timeout|Timeout|Error/,
);
});
});
diff --git a/nodejs/test/message/LiteMessage.test.ts
b/nodejs/test/message/LiteMessage.test.ts
index a964f00d..e58c874c 100644
--- a/nodejs/test/message/LiteMessage.test.ts
+++ b/nodejs/test/message/LiteMessage.test.ts
@@ -54,7 +54,7 @@ describe('Lite Message', () => {
liteTopic: 'lite-topic-name',
priority: 5,
});
- }, /priority and liteTopic should not be set at same time/);
+ }, /priority is mutually exclusive with/);
});
it('should throw error when liteTopic and messageGroup are set together', ()
=> {
@@ -65,7 +65,7 @@ describe('Lite Message', () => {
liteTopic: 'lite-topic-name',
messageGroup: 'test-group',
});
- }, /liteTopic and messageGroup should not be set at same time/);
+ }, /liteTopic is mutually exclusive with/);
});
it('should throw error when liteTopic and deliveryTimestamp are set
together', () => {
@@ -76,7 +76,7 @@ describe('Lite Message', () => {
liteTopic: 'lite-topic-name',
deliveryTimestamp: new Date(Date.now() + 60000),
});
- }, /liteTopic and deliveryTimestamp should not be set at same time/);
+ }, /liteTopic is mutually exclusive with/);
});
it('should accept empty string liteTopic', () => {
diff --git a/nodejs/test/message/PriorityMessage.test.ts
b/nodejs/test/message/PriorityMessage.test.ts
index 1721f133..104306b1 100644
--- a/nodejs/test/message/PriorityMessage.test.ts
+++ b/nodejs/test/message/PriorityMessage.test.ts
@@ -64,7 +64,7 @@ describe('Priority Message', () => {
priority: 1,
deliveryTimestamp: new Date(Date.now() + 60000),
});
- }, /priority and deliveryTimestamp should not be set at same time/);
+ }, /priority is mutually exclusive with/);
});
it('should throw error when priority and messageGroup are set together', ()
=> {
@@ -75,7 +75,7 @@ describe('Priority Message', () => {
priority: 1,
messageGroup: 'test-group',
});
- }, /priority and messageGroup should not be set at same time/);
+ }, /priority is mutually exclusive with/);
});
it('should accept priority = 0', () => {
diff --git a/nodejs/test/message/PublishingLiteMessage.test.ts
b/nodejs/test/message/PublishingLiteMessage.test.ts
index ca20319c..4c7b8d43 100644
--- a/nodejs/test/message/PublishingLiteMessage.test.ts
+++ b/nodejs/test/message/PublishingLiteMessage.test.ts
@@ -54,6 +54,6 @@ describe('PublishingMessage with LITE type', () => {
liteTopic: 'lite-topic-name',
priority: 5,
});
- }, /priority and liteTopic should not be set at same time/);
+ }, /priority is mutually exclusive with/);
});
});