This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 11b10ba2d fix(message): continue scans after unmatched pull batches 
(#6180)
11b10ba2d is described below

commit 11b10ba2db1e0f9e0f98b287ac4a71ff905d80ad
Author: zmuxuny <[email protected]>
AuthorDate: Mon Oct 12 00:16:29 2026 -0700

    fix(message): continue scans after unmatched pull batches (#6180)
    
    Both queue scan loops treated every non-`FOUND` pull status as the end of 
the queue: `RocketMQMessageProvider.java:421-424` and 
`RocketMQDLQProvider.java:500-503` break out of the scan as soon as the status 
is anything other than `FOUND`. A broker answers `NO_MATCHED_MSG` with an 
advanced `nextBeginOffset` when every message in the batch was filtered or 
already deleted, so a single unmatched batch silently discarded the remainder 
of the queue — for time-window topic queries and, throu [...]
    
    Eight new tests cover both providers. Reverting either branch loses the 
messages after the gap in `queryByTopicContinuesAfterUnmatchedBatchTest` and 
`resendMessagesContinuesAfterUnmatchedBatchTest`; reverting the counter reset 
breaks `queryByTopicResetsIllegalOffsetCounterAfterUnmatchedBatchTest` and its 
DLQ twin, because interleaved `OFFSET_ILLEGAL` results then trip 
`MAX_CONSECUTIVE_OFFSET_ILLEGAL`. 
`listMessagesStopsUnmatchedBatchesAtWindowEndTest`, 
`queryByTopicBoundsRepeatedUnmat [...]
---
 .../provider/apache/RocketMQDLQProvider.java       |   6 +
 .../provider/apache/RocketMQMessageProvider.java   |   6 +
 .../provider/apache/RocketMQDLQProviderTest.java   | 121 +++++++++++++++++++++
 .../apache/RocketMQMessageProviderTest.java        |  83 ++++++++++++++
 4 files changed, 216 insertions(+)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
index 1375fa826..2e8159c78 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
@@ -497,6 +497,12 @@ public class RocketMQDLQProvider implements DLQProvider {
                                     queue, dlqTopic, offset);
                             continue;
                         }
+                        if (pullResult.getPullStatus() == 
PullStatus.NO_MATCHED_MSG) {
+                            // The broker can skip removed messages even for a 
wildcard pull.
+                            // An advancing empty batch is not the end of the 
queue.
+                            consecutiveIllegalOffsets = 0;
+                            continue;
+                        }
                         if (pullResult.getPullStatus() != PullStatus.FOUND
                                 || pullResult.getMsgFoundList() == null) {
                             break;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
index 5ba5abd8a..c8a7ea26a 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
@@ -418,6 +418,12 @@ public class RocketMQMessageProvider implements 
MessageProvider {
                                     queue, topic, offset);
                             continue;
                         }
+                        if (pullResult.getPullStatus() == 
PullStatus.NO_MATCHED_MSG) {
+                            // The broker can skip removed messages even for a 
wildcard pull.
+                            // An advancing empty batch is not the end of the 
queue.
+                            consecutiveIllegalOffsets = 0;
+                            continue;
+                        }
                         if (pullResult.getPullStatus() != PullStatus.FOUND
                                 || pullResult.getMsgFoundList() == null) {
                             break;
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
index 0b09f6641..27ba07a9f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
@@ -822,6 +822,127 @@ class RocketMQDLQProviderTest {
                 eq("SUCCESS"));
     }
 
+    @Test
+    void listMessagesStopsUnmatchedBatchesAtWindowEndTest() throws Exception {
+        String topic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(topic, "broker-a", 0);
+        
when(pullConsumer.fetchSubscribeMessageQueues(topic)).thenReturn(Set.of(queue));
+        when(pullConsumer.searchOffset(queue, 100L)).thenReturn(10L);
+        when(pullConsumer.searchOffset(queue, 201L)).thenReturn(13L);
+        when(pullConsumer.pull(eq(queue), eq("*"), anyLong(), 
eq(32))).thenAnswer(invocation ->
+                new PullResult(PullStatus.NO_MATCHED_MSG, (Long) 
invocation.getArgument(2) + 1,
+                        0L, 13L, null));
+
+        assertThat(provider.listMessages("instance-a", "group-a", 100L, 200L, 
1, 20).getTotal()).isZero();
+
+        verify(pullConsumer, times(3)).pull(eq(queue), eq("*"), anyLong(), 
eq(32));
+    }
+
+    @Test
+    void listMessagesStopsWhenUnmatchedBatchDoesNotAdvanceTest() throws 
Exception {
+        String topic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(topic, "broker-a", 0);
+        
when(pullConsumer.fetchSubscribeMessageQueues(topic)).thenReturn(Set.of(queue));
+        when(pullConsumer.searchOffset(queue, 100L)).thenReturn(10L);
+        when(pullConsumer.searchOffset(queue, 201L)).thenReturn(13L);
+        when(pullConsumer.pull(queue, "*", 10L, 32))
+                .thenReturn(new PullResult(PullStatus.NO_MATCHED_MSG, 10L, 0L, 
13L, null));
+
+        assertThat(provider.listMessages("instance-a", "group-a", 100L, 200L, 
1, 20).getTotal()).isZero();
+
+        verify(pullConsumer, times(1)).pull(eq(queue), eq("*"), anyLong(), 
eq(32));
+    }
+
+    @Test
+    void resendMessagesContinuesAfterUnmatchedBatchTest() throws Exception {
+        String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+        MessageExt deadLetter = new MessageExt();
+        deadLetter.setMsgId("dlq-after-gap");
+        deadLetter.setTopic(dlqTopic);
+        deadLetter.setBody(new byte[] {1, 2, 3});
+        deadLetter.setStoreTimestamp(150L);
+        PullResult unmatchedBatch = new PullResult(PullStatus.NO_MATCHED_MSG, 
20L, 0L, 50L, null);
+        PullResult foundAfterGap = new PullResult(PullStatus.FOUND, 40L, 20L, 
50L, List.of(deadLetter));
+        PullResult endOfQueue = new PullResult(PullStatus.NO_NEW_MSG, 50L, 
40L, 50L, List.of());
+        SendResult sendResult = new SendResult();
+        sendResult.setSendStatus(SendStatus.SEND_OK);
+
+        
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+        when(pullConsumer.searchOffset(queue, 100L)).thenReturn(10L);
+        when(pullConsumer.searchOffset(queue, 201L)).thenReturn(50L);
+        when(pullConsumer.pull(queue, "*", 10L, 
32)).thenReturn(unmatchedBatch);
+        when(pullConsumer.pull(queue, "*", 20L, 32)).thenReturn(foundAfterGap);
+        when(pullConsumer.pull(queue, "*", 40L, 32)).thenReturn(endOfQueue);
+        when(dlqProducer.send(any(Message.class))).thenReturn(sendResult);
+        TopicList existingTargets = new TopicList();
+        existingTargets.setTopicList(Set.of("orders"));
+        when(adminExt.fetchAllTopicList()).thenReturn(existingTargets);
+        assertThat(provider.resendMessages("instance-a", "group-a", 100L, 
200L, "orders"))
+                .extracting("matched", "resent", "failed", "outcome")
+                .containsExactly(1, 1, 0, "SUCCESS");
+
+        DefaultMQPullConsumer consumer = pullConsumer;
+        verify(consumer).pull(queue, "*", 10L, 32);
+        verify(consumer).pull(queue, "*", 20L, 32);
+        verify(consumer).pull(queue, "*", 40L, 32);
+        verify(dlqProducer).send(any(Message.class));
+        verify(auditService).record(
+                eq("RESEND_DLQ"),
+                eq("DLQ"),
+                eq("group-a"),
+                isNull(),
+                contains("matched=1, resent=1, failed=0"),
+                eq("SUCCESS"));
+    }
+
+    @Test
+    void resendMessagesResetsIllegalOffsetCounterAfterUnmatchedBatchTest() 
throws Exception {
+        String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+        MessageExt deadLetter = new MessageExt();
+        deadLetter.setMsgId("dlq-after-gap");
+        deadLetter.setTopic(dlqTopic);
+        deadLetter.setBody(new byte[] {1, 2, 3});
+        deadLetter.setStoreTimestamp(150L);
+        PullResult unmatchedBatch = new PullResult(PullStatus.NO_MATCHED_MSG, 
17L, 0L, 50L, null);
+        PullResult foundAfterGap = new PullResult(PullStatus.FOUND, 40L, 20L, 
50L, List.of(deadLetter));
+        PullResult endOfQueue = new PullResult(PullStatus.NO_NEW_MSG, 50L, 
40L, 50L, List.of());
+        SendResult sendResult = new SendResult();
+        sendResult.setSendStatus(SendStatus.SEND_OK);
+
+        
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+        when(pullConsumer.searchOffset(queue, 100L)).thenReturn(10L);
+        when(pullConsumer.searchOffset(queue, 201L)).thenReturn(50L);
+        for (long offset : new long[] {10L, 11L, 12L, 17L, 18L, 19L}) {
+            when(pullConsumer.pull(queue, "*", offset, 32))
+                    .thenReturn(new PullResult(PullStatus.OFFSET_ILLEGAL, 
offset + 1, 0L, 50L, null));
+        }
+        when(pullConsumer.pull(queue, "*", 13L, 
32)).thenReturn(unmatchedBatch);
+        when(pullConsumer.pull(queue, "*", 20L, 32)).thenReturn(foundAfterGap);
+        when(pullConsumer.pull(queue, "*", 40L, 32)).thenReturn(endOfQueue);
+        when(dlqProducer.send(any(Message.class))).thenReturn(sendResult);
+        TopicList existingTargets = new TopicList();
+        existingTargets.setTopicList(Set.of("orders"));
+        when(adminExt.fetchAllTopicList()).thenReturn(existingTargets);
+        assertThat(provider.resendMessages("instance-a", "group-a", 100L, 
200L, "orders"))
+                .extracting("matched", "resent", "failed", "outcome")
+                .containsExactly(1, 1, 0, "SUCCESS");
+
+        DefaultMQPullConsumer consumer = pullConsumer;
+        verify(consumer).pull(queue, "*", 10L, 32);
+        verify(consumer).pull(queue, "*", 20L, 32);
+        verify(consumer).pull(queue, "*", 40L, 32);
+        verify(dlqProducer).send(any(Message.class));
+        verify(auditService).record(
+                eq("RESEND_DLQ"),
+                eq("DLQ"),
+                eq("group-a"),
+                isNull(),
+                contains("matched=1, resent=1, failed=0"),
+                eq("SUCCESS"));
+    }
+
     @Test
     void resendMessagesCountsNonSendOkResultsAsFailures() throws Exception {
         String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
index d2d470831..405dbee4c 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
@@ -704,6 +704,89 @@ class RocketMQMessageProviderTest {
         verify(pullConsumer).pull(queue, "*", 40L, 32);
     }
 
+    @Test
+    void queryByTopicBoundsRepeatedUnmatchedBatchesTest() throws Exception {
+        MessageQueue queue = new MessageQueue("TopicA", "broker-a", 0);
+        
when(pullConsumer.fetchSubscribeMessageQueues("TopicA")).thenReturn(Set.of(queue));
+        mockQueueWindow(pullConsumer, queue, 100L, 200L, 0L, 0L, 100L, 100L);
+        when(pullConsumer.pull(eq(queue), eq("*"), anyLong(), 
eq(32))).thenAnswer(invocation ->
+                new PullResult(PullStatus.NO_MATCHED_MSG, (Long) 
invocation.getArgument(2) + 1,
+                        0L, 100L, null));
+
+        assertThat(provider.queryMessages("instance-a", "TopicA", null, null, 
null, 100L, 200L)).isEmpty();
+
+        verify(pullConsumer, times(35)).pull(eq(queue), eq("*"), anyLong(), 
eq(32));
+    }
+
+    @Test
+    void queryByTopicStopsWhenUnmatchedBatchDoesNotAdvanceTest() throws 
Exception {
+        MessageQueue queue = new MessageQueue("TopicA", "broker-a", 0);
+        
when(pullConsumer.fetchSubscribeMessageQueues("TopicA")).thenReturn(Set.of(queue));
+        mockQueueWindow(pullConsumer, queue, 100L, 200L, 10L, 10L, 30L, 30L);
+        when(pullConsumer.pull(queue, "*", 10L, 32))
+                .thenReturn(new PullResult(PullStatus.NO_MATCHED_MSG, 10L, 0L, 
30L, null));
+
+        assertThat(provider.queryMessages("instance-a", "TopicA", null, null, 
null, 100L, 200L)).isEmpty();
+
+        verify(pullConsumer, times(1)).pull(eq(queue), eq("*"), anyLong(), 
eq(32));
+    }
+
+    @Test
+    void queryByTopicContinuesAfterUnmatchedBatchTest() throws Exception {
+        MessageQueue queue = new MessageQueue("TopicA", "broker-a", 0);
+        MessageExt message = new MessageExt();
+        message.setMsgId("msg-after-gap");
+        message.setTopic("TopicA");
+        message.setBody("payload".getBytes(StandardCharsets.UTF_8));
+        message.setStoreTimestamp(150L);
+        PullResult unmatchedBatch = new PullResult(PullStatus.NO_MATCHED_MSG, 
20L, 0L, 50L, null);
+        PullResult foundAfterGap = new PullResult(PullStatus.FOUND, 40L, 20L, 
50L, List.of(message));
+        PullResult endOfQueue = new PullResult(PullStatus.NO_NEW_MSG, 50L, 
40L, 50L, List.of());
+        
when(pullConsumer.fetchSubscribeMessageQueues("TopicA")).thenReturn(Set.of(queue));
+        mockQueueWindow(pullConsumer, queue, 100L, 200L, 10L, 10L, 50L, 50L);
+        when(pullConsumer.pull(eq(queue), eq("*"), eq(10L), 
eq(32))).thenReturn(unmatchedBatch);
+        when(pullConsumer.pull(eq(queue), eq("*"), eq(20L), 
eq(32))).thenReturn(foundAfterGap);
+        when(pullConsumer.pull(eq(queue), eq("*"), eq(40L), 
eq(32))).thenReturn(endOfQueue);
+
+        List<MessageRecordVO> messages = provider.queryMessages(
+                "instance-a", "TopicA", null, null, null, 100L, 200L);
+
+        
assertThat(messages).extracting(MessageRecordVO::getMsgId).containsExactly("msg-after-gap");
+        verify(pullConsumer).pull(queue, "*", 10L, 32);
+        verify(pullConsumer).pull(queue, "*", 20L, 32);
+        verify(pullConsumer).pull(queue, "*", 40L, 32);
+    }
+
+    @Test
+    void queryByTopicResetsIllegalOffsetCounterAfterUnmatchedBatchTest() 
throws Exception {
+        MessageQueue queue = new MessageQueue("TopicA", "broker-a", 0);
+        MessageExt message = new MessageExt();
+        message.setMsgId("msg-after-gap");
+        message.setTopic("TopicA");
+        message.setBody("payload".getBytes(StandardCharsets.UTF_8));
+        message.setStoreTimestamp(150L);
+        PullResult unmatchedBatch = new PullResult(PullStatus.NO_MATCHED_MSG, 
17L, 0L, 50L, null);
+        PullResult foundAfterGap = new PullResult(PullStatus.FOUND, 40L, 20L, 
50L, List.of(message));
+        PullResult endOfQueue = new PullResult(PullStatus.NO_NEW_MSG, 50L, 
40L, 50L, List.of());
+        
when(pullConsumer.fetchSubscribeMessageQueues("TopicA")).thenReturn(Set.of(queue));
+        mockQueueWindow(pullConsumer, queue, 100L, 200L, 10L, 10L, 50L, 50L);
+        for (long offset : new long[] {10L, 11L, 12L, 17L, 18L, 19L}) {
+            when(pullConsumer.pull(queue, "*", offset, 32))
+                    .thenReturn(new PullResult(PullStatus.OFFSET_ILLEGAL, 
offset + 1, 0L, 50L, null));
+        }
+        when(pullConsumer.pull(queue, "*", 13L, 
32)).thenReturn(unmatchedBatch);
+        when(pullConsumer.pull(eq(queue), eq("*"), eq(20L), 
eq(32))).thenReturn(foundAfterGap);
+        when(pullConsumer.pull(eq(queue), eq("*"), eq(40L), 
eq(32))).thenReturn(endOfQueue);
+
+        List<MessageRecordVO> messages = provider.queryMessages(
+                "instance-a", "TopicA", null, null, null, 100L, 200L);
+
+        
assertThat(messages).extracting(MessageRecordVO::getMsgId).containsExactly("msg-after-gap");
+        verify(pullConsumer).pull(queue, "*", 10L, 32);
+        verify(pullConsumer).pull(queue, "*", 20L, 32);
+        verify(pullConsumer).pull(queue, "*", 40L, 32);
+    }
+
     @Test
     void queryByTopicKeepsNewestMessagesWhenEarlierQueuesFillTheDefaultLimit() 
throws Exception {
         MessageQueue olderQueue = new MessageQueue("TopicA", "broker-a", 0);

Reply via email to