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 efcd32b29 fix(dlq): include all messages at the end timestamp (#5104)
efcd32b29 is described below
commit efcd32b297b1da820d023b3bc92fc9dd01c5597b
Author: 风起 <[email protected]>
AuthorDate: Thu Oct 1 17:29:19 2026 +0800
fix(dlq): include all messages at the end timestamp (#5104)
---
docs/api-spec.md | 2 +-
.../provider/apache/RocketMQDLQProvider.java | 7 +-
.../provider/apache/RocketMQDLQProviderTest.java | 94 +++++++++++++++++++---
3 files changed, 87 insertions(+), 16 deletions(-)
diff --git a/docs/api-spec.md b/docs/api-spec.md
index 4763e67bd..c39ca05b3 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1611,7 +1611,7 @@ GET
/api/dlq/{groupName}/messages?instanceId={instanceId}&startTime={ms}&endTime
| `properties` | `Record<string, string>` | 用户属性。已剔除 Broker 系统属性,按 key 排序,最多
`64` 条,单值超过 `1024` 码点时截断并追加 `...` |
| `propertiesTruncated` | `boolean` | 属性条数或单值长度是否触发了上述截断 |
-> **时间窗口**:`startTime` / `endTime`
必须同时提供或同时省略(`DLQService.validateTimeRange`)。省略时服务端使用 `[endTime - 1 小时,
endTime]`,`endTime` 再省略则取服务端当前时间。两者都为正数且 `endTime` 必须严格大于 `startTime`,否则返回
`400`。
+> **时间窗口**:`startTime` / `endTime`
必须同时提供或同时省略(`DLQService.validateTimeRange`)。消息按存储时间在闭区间 `[startTime, endTime]`
内筛选,包括与 `endTime` 同一毫秒的所有消息。省略时服务端使用 `[endTime - 1 小时, endTime]`,`endTime`
再省略则取服务端当前时间。两者都为正数且 `endTime` 必须严格大于 `startTime`,否则返回 `400`。
>
> **扫描上限**:单次扫描最多读取 `5000`
> 条死信消息(`RocketMQDLQProvider.RESEND_HARD_CAP`),命中该上限或部分队列扫描失败时结果不完整,导出接口通过响应头告知调用方。
>
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 2e2c058e4..f5c3b1085 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
@@ -439,9 +439,12 @@ public class RocketMQDLQProvider implements DLQProvider {
// must not abort the whole DLQ scan silently; skip it and
keep collecting the rest.
try {
long minOffset = consumer.searchOffset(queue, begin);
- long maxOffset = consumer.searchOffset(queue, end);
+ // searchOffset returns the first message at a timestamp.
Include
+ // every message at end, even when they span multiple pull
batches.
+ long endOffsetExclusive = end == Long.MAX_VALUE
+ ? consumer.maxOffset(queue) :
consumer.searchOffset(queue, end + 1);
int consecutiveIllegalOffsets = 0;
- for (long offset = minOffset; offset <= maxOffset; ) {
+ for (long offset = minOffset; offset < endOffsetExclusive;
) {
if (result.size() >= cap) {
truncated = true;
break outer;
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 a721a9d34..bc1620922 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
@@ -341,7 +341,7 @@ class RocketMQDLQProviderTest {
String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
- when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L);
+ when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L,
1L);
MessageExt deadLetter = new MessageExt();
deadLetter.setMsgId("dlq-msg-1");
deadLetter.setTopic("orders");
@@ -370,7 +370,7 @@ class RocketMQDLQProviderTest {
String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
- when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L);
+ when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L,
1L);
MessageExt deadLetter = new MessageExt();
deadLetter.setMsgId("dlq-msg-2");
deadLetter.setTopic("orders");
@@ -393,7 +393,7 @@ class RocketMQDLQProviderTest {
String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
- when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L);
+ when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L,
1L);
MessageExt deadLetter = new MessageExt();
deadLetter.setMsgId("dlq-msg-retry");
deadLetter.setTopic("orders");
@@ -571,7 +571,7 @@ class RocketMQDLQProviderTest {
sendResult.setSendStatus(SendStatus.SEND_OK);
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
when(pullConsumer.searchOffset(queue, 100L)).thenReturn(0L);
- when(pullConsumer.searchOffset(queue, 200L)).thenReturn(0L);
+ when(pullConsumer.searchOffset(queue, 201L)).thenReturn(1L);
when(pullConsumer.pull(queue, "*", 0L, 32)).thenReturn(pullResult);
when(dlqProducer.send(any(Message.class))).thenReturn(sendResult);
TopicList existingTargets = new TopicList();
@@ -615,7 +615,7 @@ class RocketMQDLQProviderTest {
.thenReturn(Set.of(unavailableQueue, emptyQueue));
when(pullConsumer.searchOffset(eq(unavailableQueue), anyLong()))
.thenThrow(new IllegalStateException("broker unavailable"));
- when(pullConsumer.searchOffset(eq(emptyQueue),
anyLong())).thenReturn(0L);
+ when(pullConsumer.searchOffset(eq(emptyQueue),
anyLong())).thenReturn(0L, 1L);
when(pullConsumer.pull(eq(emptyQueue), eq("*"), eq(0L),
eq(32))).thenReturn(emptyResult);
TopicList existingTargets = new TopicList();
existingTargets.setTopicList(Set.of("target-topic"));
@@ -641,7 +641,7 @@ class RocketMQDLQProviderTest {
MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
PullResult stalledResult = new PullResult(PullStatus.FOUND, 10, 0, 10,
List.of());
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
- when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(10L);
+ when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(10L,
11L);
when(pullConsumer.pull(eq(queue), eq("*"), eq(10L),
eq(32))).thenReturn(stalledResult);
TopicList existingTargets = new TopicList();
existingTargets.setTopicList(Set.of("target-topic"));
@@ -669,7 +669,7 @@ class RocketMQDLQProviderTest {
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
when(pullConsumer.searchOffset(queue, 100L)).thenReturn(10L);
- when(pullConsumer.searchOffset(queue, 200L)).thenReturn(50L);
+ when(pullConsumer.searchOffset(queue, 201L)).thenReturn(50L);
when(pullConsumer.pull(queue, "*", 10L, 32)).thenReturn(illegalOffset);
when(pullConsumer.pull(queue, "*", 20L,
32)).thenReturn(foundAfterCorrection);
when(pullConsumer.pull(queue, "*", 40L, 32)).thenReturn(endOfQueue);
@@ -710,7 +710,7 @@ class RocketMQDLQProviderTest {
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
when(pullConsumer.searchOffset(queue, 100L)).thenReturn(0L);
- when(pullConsumer.searchOffset(queue, 200L)).thenReturn(0L);
+ when(pullConsumer.searchOffset(queue, 201L)).thenReturn(1L);
when(pullConsumer.pull(queue, "*", 0L, 32)).thenReturn(pullResult);
when(dlqProducer.send(any(Message.class))).thenReturn(sendResult);
TopicList existingTargets = new TopicList();
@@ -758,7 +758,7 @@ class RocketMQDLQProviderTest {
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
when(pullConsumer.searchOffset(queue, 100L)).thenReturn(0L);
- when(pullConsumer.searchOffset(queue, 200L)).thenReturn(0L);
+ when(pullConsumer.searchOffset(queue, 201L)).thenReturn(1L);
when(pullConsumer.pull(queue, "*", 0L, 32)).thenReturn(pullResult);
when(dlqProducer.send(any(Message.class))).thenReturn(sendResult);
TopicList existingTargets = new TopicList();
@@ -878,7 +878,7 @@ class RocketMQDLQProviderTest {
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
when(pullConsumer.searchOffset(queue, 100L)).thenReturn(0L);
- when(pullConsumer.searchOffset(queue, 200L)).thenReturn(100L);
+ when(pullConsumer.searchOffset(queue, 201L)).thenReturn(101L);
when(pullConsumer.pull(queue, "*", 0L, 32)).thenReturn(pullResult);
when(dlqProducer.send(any(Message.class))).thenReturn(sendResult);
TopicList existingTargets = new TopicList();
@@ -915,7 +915,7 @@ class RocketMQDLQProviderTest {
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
when(pullConsumer.searchOffset(queue, 100L)).thenReturn(0L);
- when(pullConsumer.searchOffset(queue, 200L)).thenReturn(5001L);
+ when(pullConsumer.searchOffset(queue, 201L)).thenReturn(5001L);
when(pullConsumer.pull(queue, "*", 0L, 32)).thenReturn(pullResult);
when(dlqProducer.send(any(Message.class))).thenReturn(sendResult);
TopicList existingTargets = new TopicList();
@@ -949,7 +949,7 @@ class RocketMQDLQProviderTest {
deadLetter.setBody("hello dlq".getBytes(StandardCharsets.UTF_8));
PullResult pullResult = new PullResult(PullStatus.FOUND, 1L, 0L, 0L,
List.of(deadLetter));
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
- when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L);
+ when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L,
1L);
when(pullConsumer.pull(eq(queue), eq("*"), eq(0L),
eq(32))).thenReturn(pullResult);
DLQExportResultVO exported =
provider.exportMessages("instance-a", "group-a", 100L, 200L,
1000);
@@ -975,7 +975,7 @@ class RocketMQDLQProviderTest {
String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
- when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L);
+ when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L,
1L);
when(pullConsumer.pull(eq(queue), eq("*"), eq(0L), eq(32)))
.thenReturn(new PullResult(PullStatus.NO_NEW_MSG, 1L, 0L, 0L,
List.of()));
// maxCount=0 falls back to the hard cap instead of failing; scan
still completes.
@@ -985,6 +985,74 @@ class RocketMQDLQProviderTest {
assertThat(exported.getLimit()).isEqualTo(5000);
}
+ @Test
+ void exportMessagesIncludesAllMessagesAtEndTimestampTest() throws
Exception {
+ String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+ MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+ List<MessageExt> stored = IntStream.range(0, 81).mapToObj(index -> {
+ MessageExt message = new MessageExt();
+ message.setMsgId("boundary-" + index);
+ message.setTopic(dlqTopic);
+ message.setQueueOffset(index);
+ message.setStoreTimestamp(index < 80 ? 200L : 201L);
+ return message;
+ }).toList();
+
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+ // Real lower-bound semantics: 80 messages share the inclusive end
time.
+ when(pullConsumer.searchOffset(eq(queue), anyLong()))
+ .thenAnswer(invocation -> (long) invocation.getArgument(1) <=
200L ? 0L : 80L);
+ when(pullConsumer.pull(eq(queue), eq("*"), anyLong(),
eq(32))).thenAnswer(invocation -> {
+ int offset = ((Long) invocation.getArgument(2)).intValue();
+ int next = Math.min(offset + 32, stored.size());
+ return new PullResult(PullStatus.FOUND, next, 0L, stored.size(),
stored.subList(offset, next));
+ });
+
+ DLQExportResultVO result = provider.exportMessages("instance-a",
"group-a", 100L, 200L, 1000);
+
+ assertThat(result.getMessages()).extracting(DLQMessageVO::getMsgId)
+ .containsExactlyElementsOf(IntStream.range(0,
80).mapToObj(index -> "boundary-" + index).toList());
+ assertThat(result.isTruncated()).isFalse();
+ assertThat(result.getFailedQueueCount()).isZero();
+ verify(pullConsumer).searchOffset(queue, 201L);
+ verify(pullConsumer, times(3)).pull(eq(queue), eq("*"), anyLong(),
eq(32));
+ }
+
+ @Test
+ void exportMessagesSkipsAnEmptyOffsetWindowTest() throws Exception {
+ String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+ MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+ when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(7L);
+
+ DLQExportResultVO result = provider.exportMessages("instance-a",
"group-a", 100L, 200L, 1000);
+
+ assertThat(result.getMessages()).isEmpty();
+ assertThat(result.getFailedQueueCount()).isZero();
+ assertThat(result.isTruncated()).isFalse();
+ verify(pullConsumer, never()).pull(any(), anyString(), anyLong(),
anyInt());
+ }
+
+ @Test
+ void exportMessagesUsesQueueEndForMaximumTimestampTest() throws Exception {
+ String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+ MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+ MessageExt message = new MessageExt();
+ message.setMsgId("last-message");
+ message.setTopic(dlqTopic);
+ message.setStoreTimestamp(150L);
+
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+ when(pullConsumer.searchOffset(queue, 100L)).thenReturn(0L);
+ when(pullConsumer.maxOffset(queue)).thenReturn(1L);
+ when(pullConsumer.pull(queue, "*", 0L, 32))
+ .thenReturn(new PullResult(PullStatus.FOUND, 1L, 0L, 1L,
List.of(message)));
+
+ DLQExportResultVO result = provider.exportMessages("instance-a",
"group-a", 100L, Long.MAX_VALUE, 1000);
+
+
assertThat(result.getMessages()).extracting(DLQMessageVO::getMsgId).containsExactly("last-message");
+ verify(pullConsumer).maxOffset(queue);
+ verify(pullConsumer, never()).searchOffset(queue, Long.MIN_VALUE);
+ }
+
private void stubExistingTarget(String topic) throws Exception {
TopicList existingTargets = new TopicList();
existingTargets.setTopicList(Set.of(topic));