This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/rocketmq.git
The following commit(s) were added to refs/heads/develop by this push:
new 6291ce1ed3 [ISSUE #11191] Fix Lite topic consumer losing messages on
event re-dispatch during a pop (#11192)
6291ce1ed3 is described below
commit 6291ce1ed398f5eb2571006d8964ac7afa2adcd4
Author: Quan <[email protected]>
AuthorDate: Tue Sep 22 11:07:00 2026 +0800
[ISSUE #11191] Fix Lite topic consumer losing messages on event re-dispatch
during a pop (#11192)
- Mark the dedup entry only after a lite topic is handled under the pop
lock, not when it is FIFO-blocked, so a re-dispatched event is retried instead
of dropped
- Move the processed-set marking into popLiteTopic and narrow its visibility
- Add a regression test reproducing the concurrent ack-unblock event loss
---
.../broker/processor/PopLiteMessageProcessor.java | 15 +-
.../PopLiteMessageProcessorEventLossTest.java | 353 +++++++++++++++++++++
.../processor/PopLiteMessageProcessorTest.java | 48 ++-
3 files changed, 395 insertions(+), 21 deletions(-)
diff --git
a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessor.java
b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessor.java
index 167597dab3..8fd3797437 100644
---
a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessor.java
+++
b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessor.java
@@ -257,8 +257,10 @@ public class PopLiteMessageProcessor implements
NettyRequestProcessor {
if (null == lmqName) {
break;
}
- if (!processed.add(lmqName)) {
- continue; // wait for next pop request or re-fetch in current
process, here prefer the former approach
+ if (processed.contains(lmqName)) {
+ // Already handled in this pop; skip the duplicate. A
FIFO-blocked lmq is never marked by
+ // popLiteTopic, so its later ack-unblock re-dispatch is
retried instead of being deduped away.
+ continue;
}
// Tombstone check: reject pull if this client was evicted from
the liteTopic (exclusive mode)
if (isExclusiveGroup &&
brokerController.getLiteSubscriptionRegistry().hasExclusiveEvictionTombstone(clientId,
lmqName)) {
@@ -266,7 +268,7 @@ public class PopLiteMessageProcessor implements
NettyRequestProcessor {
continue;
}
Pair<StringBuilder, GetMessageResult> pair =
popLiteTopic(parentTopic, clientHost, group, lmqName,
- maxNum - total.get(), popTime, invisibleTime, attemptId);
+ maxNum - total.get(), popTime, invisibleTime, attemptId,
processed);
if (null == pair || pair.getObject2().getMessageCount() <= 0) {
continue;
}
@@ -285,16 +287,19 @@ public class PopLiteMessageProcessor implements
NettyRequestProcessor {
}
@VisibleForTesting
- public Pair<StringBuilder, GetMessageResult> popLiteTopic(String
parentTopic, String clientHost, String group,
- String lmqName, long maxNum, long popTime, long invisibleTime, String
attemptId) {
+ Pair<StringBuilder, GetMessageResult> popLiteTopic(String parentTopic,
String clientHost, String group,
+ String lmqName, long maxNum, long popTime, long invisibleTime, String
attemptId, Set<String> processed) {
String lockKey = KeyBuilder.buildPopLiteLockKey(group, lmqName);
if (!lockService.tryLock(lockKey)) {
return null;
}
try {
if (isFifoBlocked(attemptId, group, lmqName, invisibleTime)) {
+ // Leave this lmq unmarked so a later ack-unblock re-dispatch
is retried, not deduped away.
return null;
}
+ // Holding the lock and not blocked: this lmq is handled in this
pop, mark it to dedup further events.
+ processed.add(lmqName);
final long consumeOffset = getPopOffset(group, lmqName);
GetMessageResult result = getMessage(clientHost, group, lmqName,
consumeOffset, (int) maxNum);
return handleGetMessageResult(result, parentTopic, group, lmqName,
popTime, invisibleTime, attemptId);
diff --git
a/broker/src/test/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessorEventLossTest.java
b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessorEventLossTest.java
new file mode 100644
index 0000000000..6d3d28d1df
--- /dev/null
+++
b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessorEventLossTest.java
@@ -0,0 +1,353 @@
+/*
+ * 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.
+ */
+
+package org.apache.rocketmq.broker.processor;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.rocketmq.broker.BrokerController;
+import org.apache.rocketmq.broker.lite.AbstractLiteLifecycleManager;
+import org.apache.rocketmq.broker.lite.LiteEventDispatcher;
+import org.apache.rocketmq.broker.lite.LiteSubscriptionRegistry;
+import org.apache.rocketmq.broker.offset.ConsumerOffsetManager;
+import org.apache.rocketmq.broker.pop.PopConsumerLockService;
+import org.apache.rocketmq.broker.pop.orderly.ConsumerOrderInfoManager;
+import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager;
+import org.apache.rocketmq.common.BrokerConfig;
+import org.apache.rocketmq.common.KeyBuilder;
+import org.apache.rocketmq.common.Pair;
+import org.apache.rocketmq.common.TopicConfig;
+import org.apache.rocketmq.common.UtilAll;
+import org.apache.rocketmq.common.entity.ClientGroup;
+import org.apache.rocketmq.common.lite.LiteSubscription;
+import org.apache.rocketmq.common.lite.LiteUtil;
+import
org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
+import org.apache.rocketmq.store.GetMessageResult;
+import org.apache.rocketmq.store.MessageStore;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+/**
+ * Reproduces the Lite pop event-loss bug: within one popByClientId, a lmq
already visited in this
+ * request (added to the {@code processed} dedup set) gets re-dispatched by a
concurrent ack that
+ * advances the offset and unblocks FIFO. The buggy code drops the re-enqueued
event, so its
+ * remaining messages are never read (queue becomes empty, offset does not
advance further).
+ *
+ * <p>Symptom asserted per iteration: consumer subscribing lt1 and lt2 with 5
messages each must
+ * eventually receive all 5+5. On the buggy code lt1 only yields its first
message (offset 0) while
+ * lt2 yields all 5 (6 total), matching the failing E2E case.
+ *
+ * <p>The harness boots real components once ({@link MessageStore}, {@link
LiteEventDispatcher},
+ * {@link PopLiteMessageProcessor} with its internal FIFO order-info + lock
service,
+ * {@link ConsumerOffsetManager}) and loops the send/consume scenario
internally. The ack of lt1's
+ * first message is fired while popByClientId is mid-iteration through a
test-only white-box spy on
+ * {@code popLiteTopic}; this is purely to make the concurrency race
deterministic rather than
+ * timing-dependent, and is not a dependency of the production code path.
+ */
+public class PopLiteMessageProcessorEventLossTest {
+
+ private static final String PARENT_TOPIC = "LitePopLossParent";
+ private static final String GROUP = "LitePopLossGroup";
+ private static final String CLIENT_ID = "clientId-repro";
+ private static final String CLIENT_HOST = "127.0.0.1:0";
+ private static final long INVISIBLE = 60_000L;
+ private static final int MAX_NUM = 32;
+ private static final int ITERATIONS = 5;
+ private static final int DRAIN_ROUNDS = 6;
+
+ private static final BrokerConfig BROKER_CONFIG = new BrokerConfig();
+ private static final ConcurrentMap<String, TopicConfig> TOPIC_CONFIG_TABLE
= new ConcurrentHashMap<>();
+
+ private static String storePathRootDir;
+ private static MessageStore messageStore;
+ private static ConsumerOffsetManager consumerOffsetManager;
+ private static LiteEventDispatcher liteEventDispatcher;
+ private static PopLiteMessageProcessor popLiteMessageProcessor;
+ private static LiteSubscriptionRegistry liteSubscriptionRegistry;
+
+ // Per-pop capture populated by the popLiteTopic spy: lmqName -> offsets
read in the current pop.
+ private static final Map<String, List<Long>> LAST_POP_READS = new
HashMap<>();
+ // Cumulative reception across the whole scenario: lmqName -> offsets
RECEIVED.
+ private static final Map<String, Set<Long>> RECEIVED = new HashMap<>();
+
+ // Injection state: when armed, firing the ack of (injectLmq, offset 0)
while popLiteTopic runs.
+ private static final AtomicBoolean INJECT_ARMED = new AtomicBoolean(false);
+ private static volatile String injectLmq;
+ private static volatile long injectPopTime;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ storePathRootDir = System.getProperty("java.io.tmpdir") +
File.separator + "store-litePopLoss";
+ UtilAll.deleteFile(new File(storePathRootDir));
+
+ BROKER_CONFIG.setMaxClientEventCount(1000);
+
+ BrokerController brokerController =
Mockito.mock(BrokerController.class);
+ SubscriptionGroupManager subscriptionGroupManager =
Mockito.mock(SubscriptionGroupManager.class);
+ AbstractLiteLifecycleManager liteLifecycleManager =
Mockito.mock(AbstractLiteLifecycleManager.class);
+ liteSubscriptionRegistry =
Mockito.mock(LiteSubscriptionRegistry.class);
+ PopMessageProcessor popMessageProcessor =
Mockito.mock(PopMessageProcessor.class);
+
+ SubscriptionGroupConfig groupConfig = new SubscriptionGroupConfig();
+ groupConfig.setGroupName(GROUP);
+ groupConfig.setLiteBindTopic(PARENT_TOPIC);
+
when(subscriptionGroupManager.findSubscriptionGroupConfig(GROUP)).thenReturn(groupConfig);
+
+ doReturn(BROKER_CONFIG).when(brokerController).getBrokerConfig();
+
doReturn(subscriptionGroupManager).when(brokerController).getSubscriptionGroupManager();
+
doReturn(liteSubscriptionRegistry).when(brokerController).getLiteSubscriptionRegistry();
+
doReturn(liteLifecycleManager).when(brokerController).getLiteLifecycleManager();
+
doReturn(popMessageProcessor).when(brokerController).getPopMessageProcessor();
+
when(liteLifecycleManager.getMaxOffsetInQueue(anyString())).thenReturn(1_000_000L);
+
+ consumerOffsetManager = new ConsumerOffsetManager(brokerController);
+
doReturn(consumerOffsetManager).when(brokerController).getConsumerOffsetManager();
+
+ messageStore =
org.apache.rocketmq.broker.lite.LiteTestUtil.buildMessageStore(
+ storePathRootDir, BROKER_CONFIG, TOPIC_CONFIG_TABLE, false, null);
+ doReturn(messageStore).when(brokerController).getMessageStore();
+ messageStore.load();
+ messageStore.start();
+
+ liteEventDispatcher = new LiteEventDispatcher(brokerController,
liteSubscriptionRegistry, liteLifecycleManager);
+ popLiteMessageProcessor = Mockito.spy(new
PopLiteMessageProcessor(brokerController, liteEventDispatcher));
+ NotificationProcessor notificationProcessor = new
NotificationProcessor(brokerController);
+
+
doReturn(liteEventDispatcher).when(brokerController).getLiteEventDispatcher();
+
doReturn(popLiteMessageProcessor).when(brokerController).getPopLiteMessageProcessor();
+
doReturn(notificationProcessor).when(brokerController).getNotificationProcessor();
+
+ // Subscribers fan-out: any lmq under GROUP resolves to our single
client.
+ Map<String, List<ClientGroup>> subscriberMap = new HashMap<>();
+ subscriberMap.put(GROUP, Collections.singletonList(new
ClientGroup(CLIENT_ID, GROUP)));
+ when(liteSubscriptionRegistry.getAllSubscribers(eq(GROUP),
anyString())).thenReturn(subscriberMap);
+
when(liteSubscriptionRegistry.hasExclusiveEvictionTombstone(anyString(),
anyString())).thenReturn(false);
+
+ // Avoid metrics manager dependency.
+ doNothing().when(popLiteMessageProcessor).recordPopLiteMetrics(any(),
anyString(), anyString());
+
+ // Spy hook on popLiteTopic: capture per-lmq offsets, and inject the
mid-pop ack when armed.
+ Mockito.doAnswer(invocation -> {
+ Object real = invocation.callRealMethod();
+ String lmq = invocation.getArgument(3);
+ @SuppressWarnings("unchecked")
+ Pair<StringBuilder, GetMessageResult> pair = (Pair<StringBuilder,
GetMessageResult>) real;
+ if (pair != null && pair.getObject2() != null &&
pair.getObject2().getMessageCount() > 0) {
+ List<Long> offsets = new
ArrayList<>(pair.getObject2().getMessageQueueOffset());
+ synchronized (LAST_POP_READS) {
+ LAST_POP_READS.computeIfAbsent(lmq, k -> new
ArrayList<>()).addAll(offsets);
+ }
+ synchronized (RECEIVED) {
+ RECEIVED.computeIfAbsent(lmq, k -> new
HashSet<>()).addAll(offsets);
+ }
+ }
+ if (INJECT_ARMED.get() && lmq.equals(injectLmq)) {
+ INJECT_ARMED.set(false);
+ // Fire the ack of the first in-flight message concurrently
with this pop. This advances
+ // the offset, unblocks FIFO and re-dispatches the lmq back
into this client's queue,
+ // reproducing a duplicate for an already-visited lmq within
the same popByClientId.
+ ackLite(injectLmq, 0L, injectPopTime);
+ }
+ return real;
+ }).when(popLiteMessageProcessor).popLiteTopic(anyString(),
anyString(), anyString(), anyString(),
+ anyLong(), anyLong(), anyLong(), anyString(), any());
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ if (messageStore != null) {
+ messageStore.shutdown();
+ messageStore.destroy();
+ }
+ UtilAll.deleteFile(new File(storePathRootDir));
+ }
+
+ @Test
+ public void testLitePopEventLossAcrossIterations() throws Exception {
+ for (int i = 0; i < ITERATIONS; i++) {
+ runOnce(i);
+ }
+ }
+
+ private void runOnce(int iteration) throws Exception {
+ String lt1 = "lt1-" + iteration + "-" + UUID.randomUUID();
+ String lt2 = "lt2-" + iteration + "-" + UUID.randomUUID();
+ String lmq1 = LiteUtil.toLmqName(PARENT_TOPIC, lt1);
+ String lmq2 = LiteUtil.toLmqName(PARENT_TOPIC, lt2);
+
+ RECEIVED.clear();
+
+ // lt1 initially has only its first message readable; lt2 has all five.
+ send(lt1, 1);
+ send(lt2, 5);
+ awaitReput(lmq1, 1);
+ awaitReput(lmq2, 5);
+
+ // Initialize consumer offsets so getPopOffset does not fall back to
init-offset lookup.
+ consumerOffsetManager.commitOffset("init", GROUP, lmq1, 0, 0L);
+ consumerOffsetManager.commitOffset("init", GROUP, lmq2, 0, 0L);
+
+ // The full subscription is used only by the fix's recovery path.
+ LiteSubscription subscription = new
LiteSubscription().setGroup(GROUP).setTopic(PARENT_TOPIC);
+ Set<String> lmqSet = new HashSet<>();
+ lmqSet.add(lmq1);
+ lmqSet.add(lmq2);
+ subscription.setLmqSet(lmqSet);
+
when(liteSubscriptionRegistry.getLiteSubscription(CLIENT_ID)).thenReturn(subscription);
+
+ enqueue(lmq1);
+ enqueue(lmq2);
+
+ // POP #1: reads lt1 offset 0 (now in-flight, FIFO-blocked) and lt2
offsets 0..4.
+ long popTime1 = System.currentTimeMillis();
+ pop(popTime1);
+
+ // lt1's remaining four messages become readable now; a single
coalesced arrival event enqueues lt1.
+ send(lt1, 4);
+ awaitReput(lmq1, 5);
+ enqueue(lmq1);
+
+ // POP #2: visits lt1 (blocked -> 0 read, added to processed); the spy
fires lt1's offset-0 ack
+ // mid-pop, which re-dispatches lt1. The buggy code drops the
re-enqueued duplicate.
+ injectLmq = lmq1;
+ injectPopTime = popTime1;
+ INJECT_ARMED.set(true);
+ long popTime2 = System.currentTimeMillis() + 1;
+ pop(popTime2);
+ INJECT_ARMED.set(false);
+
+ // Drain: keep popping and acking; on the fixed code lt1 is recovered
and yields offsets 1..4.
+ for (int r = 0; r < DRAIN_ROUNDS; r++) {
+ Set<Long> lt1Received;
+ synchronized (RECEIVED) {
+ lt1Received = new HashSet<>(RECEIVED.getOrDefault(lmq1,
Collections.emptySet()));
+ }
+ if (lt1Received.size() >= 5) {
+ break;
+ }
+ long popTime = System.currentTimeMillis() + 2 + r;
+ Map<String, List<Long>> reads = pop(popTime);
+ for (Map.Entry<String, List<Long>> e : reads.entrySet()) {
+ for (Long off : e.getValue()) {
+ ackLite(e.getKey(), off, popTime);
+ }
+ }
+ }
+
+ assertAllReceived(iteration, lmq1, lmq2);
+ }
+
+ private Map<String, List<Long>> pop(long popTime) {
+ synchronized (LAST_POP_READS) {
+ LAST_POP_READS.clear();
+ }
+ popLiteMessageProcessor.popByClientId(CLIENT_HOST, PARENT_TOPIC,
GROUP, CLIENT_ID,
+ popTime, INVISIBLE, MAX_NUM, UUID.randomUUID().toString());
+ synchronized (LAST_POP_READS) {
+ Map<String, List<Long>> copy = new HashMap<>();
+ for (Map.Entry<String, List<Long>> e : LAST_POP_READS.entrySet()) {
+ copy.put(e.getKey(), new ArrayList<>(e.getValue()));
+ }
+ return copy;
+ }
+ }
+
+ private void enqueue(String lmqName) {
+ liteEventDispatcher.tryDispatchToClient(lmqName, CLIENT_ID, GROUP,
false);
+ }
+
+ private void send(String liteTopic, int count) {
+ for (int i = 0; i < count; i++) {
+
messageStore.putMessage(org.apache.rocketmq.broker.lite.LiteTestUtil.buildMessage(PARENT_TOPIC,
liteTopic));
+ }
+ }
+
+ private void awaitReput(String lmqName, long expectedMaxOffset) throws
Exception {
+ long deadline = System.currentTimeMillis() + 5_000L;
+ while (System.currentTimeMillis() < deadline) {
+ if (messageStore.getMaxOffsetInQueue(lmqName, 0) >=
expectedMaxOffset) {
+ return;
+ }
+ Thread.sleep(10);
+ }
+ Assert.fail("reput timeout for " + lmqName + ", expected max offset "
+ expectedMaxOffset
+ + ", actual " + messageStore.getMaxOffsetInQueue(lmqName, 0));
+ }
+
+ /**
+ * Replicates {@link AckMessageProcessor#ackLite} for a single lite
message: advance the FIFO
+ * order info, commit the consumer offset, and re-dispatch the lmq when it
becomes unblocked.
+ */
+ private static void ackLite(String lmqName, long ackOffset, long popTime) {
+ ConsumerOrderInfoManager orderInfoManager =
popLiteMessageProcessor.getConsumerOrderInfoManager();
+ PopConsumerLockService lockService =
popLiteMessageProcessor.getLockService();
+ String lockKey = KeyBuilder.buildPopLiteLockKey(GROUP, lmqName);
+ while (!lockService.tryLock(lockKey)) {
+ // spin, matching AckMessageProcessor.ackLite
+ }
+ try {
+ long nextOffset = orderInfoManager.commitAndNext(lmqName, GROUP,
0, ackOffset, popTime);
+ if (nextOffset > -1L) {
+ if (!consumerOffsetManager.hasOffsetReset(lmqName, GROUP, 0)) {
+ consumerOffsetManager.commitOffset("AckLiteHost", GROUP,
lmqName, 0, nextOffset);
+ }
+ if (!orderInfoManager.checkBlock(null, lmqName, GROUP, 0,
INVISIBLE)) {
+ liteEventDispatcher.dispatch(GROUP, lmqName, 0,
nextOffset, -1);
+ }
+ }
+ } finally {
+ lockService.unlock(lockKey);
+ }
+ }
+
+ private void assertAllReceived(int iteration, String lmq1, String lmq2) {
+ Set<Long> expected = new HashSet<>();
+ for (long o = 0; o < 5; o++) {
+ expected.add(o);
+ }
+ Set<Long> lt1;
+ Set<Long> lt2;
+ synchronized (RECEIVED) {
+ lt1 = new HashSet<>(RECEIVED.getOrDefault(lmq1,
Collections.emptySet()));
+ lt2 = new HashSet<>(RECEIVED.getOrDefault(lmq2,
Collections.emptySet()));
+ }
+ Assert.assertEquals("iteration " + iteration + ": lt2 must receive 5
messages", expected, lt2);
+ Assert.assertEquals("iteration " + iteration + ": lt1 must receive 5
messages but got " + lt1
+ + " (event loss reproduced: lt1 stuck after its first message)",
expected, lt1);
+ }
+}
diff --git
a/broker/src/test/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessorTest.java
b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessorTest.java
index ee6d6101b1..7a3fca9931 100644
---
a/broker/src/test/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessorTest.java
+++
b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopLiteMessageProcessorTest.java
@@ -53,6 +53,8 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Iterator;
+import java.util.HashSet;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@@ -63,7 +65,10 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
@@ -230,14 +235,14 @@ public class PopLiteMessageProcessorTest {
when(liteEventDispatcher.getEventIterator("clientId")).thenReturn(mockIterator);
doReturn(new Pair<>(new StringBuilder("0"), mockResult))
.when(popLiteMessageProcessor)
- .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString());
+ .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString(), anySet());
Pair<StringBuilder, GetMessageResult> result =
popLiteMessageProcessor.popByClientId(
"clientHost", "parentTopic", "group", "clientId", pollTime, 6000L,
32, "attemptId");
assertEquals(msgCount, result.getObject2().getMessageCount());
verify(mockIterator, times(2)).hasNext();
- verify(popLiteMessageProcessor).popLiteTopic("parentTopic"
,"clientHost", "group", event, 32L, pollTime, 6000L, "attemptId");
+ verify(popLiteMessageProcessor).popLiteTopic(eq("parentTopic"),
eq("clientHost"), eq("group"), eq(event), eq(32L), eq(pollTime), eq(6000L),
eq("attemptId"), anySet());
}
@SuppressWarnings("unchecked")
@@ -255,7 +260,7 @@ public class PopLiteMessageProcessorTest {
when(liteEventDispatcher.getEventIterator("clientId")).thenReturn(mockIterator);
doReturn(new Pair<>(new StringBuilder("0"), mockResult))
.when(popLiteMessageProcessor)
- .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString());
+ .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString(), anySet());
Pair<StringBuilder, GetMessageResult> result =
popLiteMessageProcessor.popByClientId(
"clientHost", "parentTopic", "group", "clientId", pollTime, 6000L,
2, "attemptId");
@@ -263,8 +268,8 @@ public class PopLiteMessageProcessorTest {
assertEquals(2, result.getObject2().getMessageCount());
assertEquals("0;0", result.getObject1().toString());
verify(mockIterator, times(2)).hasNext();
- verify(popLiteMessageProcessor).popLiteTopic("parentTopic",
"clientHost", "group", event1, 2L, pollTime, 6000L, "attemptId");
- verify(popLiteMessageProcessor).popLiteTopic("parentTopic",
"clientHost", "group", event2, 1L, pollTime, 6000L, "attemptId");
+ verify(popLiteMessageProcessor).popLiteTopic(eq("parentTopic"),
eq("clientHost"), eq("group"), eq(event1), eq(2L), eq(pollTime), eq(6000L),
eq("attemptId"), anySet());
+ verify(popLiteMessageProcessor).popLiteTopic(eq("parentTopic"),
eq("clientHost"), eq("group"), eq(event2), eq(1L), eq(pollTime), eq(6000L),
eq("attemptId"), anySet());
}
@SuppressWarnings("unchecked")
@@ -281,9 +286,14 @@ public class PopLiteMessageProcessorTest {
when(mockIterator.hasNext()).thenReturn(true, true, true, false);
when(mockIterator.next()).thenReturn(event1, event2, event3);
when(liteEventDispatcher.getEventIterator("clientId")).thenReturn(mockIterator);
- doReturn(new Pair<>(new StringBuilder("0"), mockResult))
- .when(popLiteMessageProcessor)
- .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString());
+ // popLiteTopic now owns the processed mark (added under the lock when
not blocked); the stub
+ // mirrors that so the caller's contains-based dedup can drop the
duplicate third event.
+ doAnswer(invocation -> {
+ Set<String> processed = invocation.getArgument(8);
+ processed.add(invocation.getArgument(3));
+ return new Pair<>(new StringBuilder("0"), mockResult);
+ }).when(popLiteMessageProcessor)
+ .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString(), anySet());
Pair<StringBuilder, GetMessageResult> result =
popLiteMessageProcessor.popByClientId(
"clientHost", "parentTopic", "group", "clientId", pollTime, 6000L,
32, "attemptId");
@@ -291,8 +301,8 @@ public class PopLiteMessageProcessorTest {
assertEquals(2, result.getObject2().getMessageCount());
assertEquals("0;0", result.getObject1().toString());
verify(mockIterator, times(4)).hasNext();
- verify(popLiteMessageProcessor).popLiteTopic("parentTopic",
"clientHost", "group", event1, 32L, pollTime, 6000L, "attemptId");
- verify(popLiteMessageProcessor).popLiteTopic("parentTopic",
"clientHost", "group", event2, 31L, pollTime, 6000L, "attemptId");
+ verify(popLiteMessageProcessor).popLiteTopic(eq("parentTopic"),
eq("clientHost"), eq("group"), eq(event1), eq(32L), eq(pollTime), eq(6000L),
eq("attemptId"), anySet());
+ verify(popLiteMessageProcessor).popLiteTopic(eq("parentTopic"),
eq("clientHost"), eq("group"), eq(event2), eq(31L), eq(pollTime), eq(6000L),
eq("attemptId"), anySet());
}
@Test
@@ -360,10 +370,12 @@ public class PopLiteMessageProcessorTest {
public void testPopLiteTopic_lockFailed() {
when(lockService.tryLock(anyString())).thenReturn(false);
+ Set<String> processed = new HashSet<>();
Pair<StringBuilder, GetMessageResult> result =
popLiteMessageProcessor.popLiteTopic("parentTopic",
- "clientHost", "group", "lmqName", 32L, System.currentTimeMillis(),
6000L, "attemptId");
+ "clientHost", "group", "lmqName", 32L, System.currentTimeMillis(),
6000L, "attemptId", processed);
assertNull(result);
+ assertFalse(processed.contains("lmqName"));
verify(lockService).tryLock(anyString());
verify(lockService, never()).unlock(anyString());
}
@@ -374,10 +386,12 @@ public class PopLiteMessageProcessorTest {
when(consumerOrderInfoManager.checkBlock(anyString(), anyString(),
anyString(), anyInt(), anyLong()))
.thenReturn(true);
+ Set<String> processed = new HashSet<>();
Pair<StringBuilder, GetMessageResult> result =
popLiteMessageProcessor.popLiteTopic("parentTopic",
- "clientHost", "group", "lmqName", 32L, System.currentTimeMillis(),
6000L, "attemptId");
+ "clientHost", "group", "lmqName", 32L, System.currentTimeMillis(),
6000L, "attemptId", processed);
assertThat(result).isNull();
+ assertFalse(processed.contains("lmqName"));
verify(lockService).tryLock(anyString());
verify(lockService).unlock(anyString());
}
@@ -390,10 +404,12 @@ public class PopLiteMessageProcessorTest {
GetMessageResult mockResult =
mockGetMessageResult(GetMessageStatus.FOUND, 1, 100L);
when(messageStore.getMessage("group", "lmqName", 0, 0, 32,
null)).thenReturn(mockResult);
+ Set<String> processed = new HashSet<>();
Pair<StringBuilder, GetMessageResult> result =
popLiteMessageProcessor.popLiteTopic("parentTopic",
- "clientHost", "group", "lmqName", 32L, System.currentTimeMillis(),
6000L, "attemptId");
+ "clientHost", "group", "lmqName", 32L, System.currentTimeMillis(),
6000L, "attemptId", processed);
assertEquals(mockResult, result.getObject2());
+ assertTrue(processed.contains("lmqName"));
verify(lockService).tryLock(anyString());
verify(lockService).unlock(anyString());
}
@@ -512,7 +528,7 @@ public class PopLiteMessageProcessorTest {
assertEquals(0, result.getObject2().getMessageCount());
// popLiteTopic should never be called for the tombstoned lmqName
verify(popLiteMessageProcessor, never())
- .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString());
+ .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString(), anySet());
}
@SuppressWarnings("unchecked")
@@ -540,13 +556,13 @@ public class PopLiteMessageProcessorTest {
when(liteEventDispatcher.getEventIterator(clientId)).thenReturn(mockIterator);
doReturn(new Pair<>(new StringBuilder("0"), mockResult))
.when(popLiteMessageProcessor)
- .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString());
+ .popLiteTopic(anyString(), anyString(), anyString(), anyString(),
anyLong(), anyLong(), anyLong(), anyString(), anySet());
Pair<StringBuilder, GetMessageResult> result =
popLiteMessageProcessor.popByClientId(
"clientHost", "parentTopic", group, clientId, pollTime, 6000L, 32,
"attemptId");
// Should return the message since no tombstone blocks
assertEquals(msgCount, result.getObject2().getMessageCount());
- verify(popLiteMessageProcessor).popLiteTopic("parentTopic",
"clientHost", group, lmqName, 32L, pollTime, 6000L, "attemptId");
+ verify(popLiteMessageProcessor).popLiteTopic(eq("parentTopic"),
eq("clientHost"), eq(group), eq(lmqName), eq(32L), eq(pollTime), eq(6000L),
eq("attemptId"), anySet());
}
}