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 529ba3ed5 fix(dlq): bound retained bodies before read conversion 
(#5365)
529ba3ed5 is described below

commit 529ba3ed54d1aabdf97f8ccd763356e0281140d0
Author: zmuxuny <[email protected]>
AuthorDate: Mon Oct 19 03:54:48 2026 -0700

    fix(dlq): bound retained bodies before read conversion (#5365)
    
    Verified against `rocketmq-studio` at f81e73a49 before merging: 
缺陷仍在(`RocketMQDLQProvider.java:341-346` `listMessages` 先全量 
`map(this::toExportVO)` 再 `subList`;`scanDeadLetters`(:444) 无字节预算,全文无 
`retainedBodyBytes` /
    
    merge-tree clean against the current trunk. Merged as-is (squash) per the 
maintainer review of 2026-10-19.
---
 docs/api-spec.md                                   |   6 +
 .../provider/apache/RocketMQDLQProvider.java       |  44 ++-
 .../studio/instance/dlq/DLQControllerTest.java     |  32 +++
 .../apache/RocketMQDLQPayloadBudgetTest.java       | 299 +++++++++++++++++++++
 4 files changed, 370 insertions(+), 11 deletions(-)

diff --git a/docs/api-spec.md b/docs/api-spec.md
index fa0d30721..3efa974b0 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1839,6 +1839,12 @@ POST /api/messages/direct-consume
 
 ## 9. 死信队列 Dead Letter Queue
 
+DLQ 消息列表、JSON 导出和 Excel 导出在扫描时共享每次请求 **10 MiB 原始消息体**的累积上限。
+超过上限会返回 `413` 和提示 `DLQ read exceeds the 10 MiB message body limit; narrow the 
time range`,
+不会返回截断的消息体或部分成功的下载。请缩小时间窗口,或在 JSON 导出中减小 `maxCount`。
+Excel 的 `msgIds` 过滤在扫描后执行,因此扫描本身仍受此上限约束。原有条数上限和重发行为不变。
+此限制不包含消息属性、客户端已解码的单个拉取批次或并发请求;它不是整个 JVM 的内存上限。
+
 ### 9.1 获取 DLQ 列表
 
 ```
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 c440a4590..d64ca19c6 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
@@ -89,6 +89,9 @@ public class RocketMQDLQProvider implements DLQProvider {
 
     private static final long ONE_HOUR_MILLIS = 3600_000L;
     private static final int RESEND_HARD_CAP = 5000;
+    // Bounds retained raw bodies before read-side UTF-8/Base64 expansion. 
This is not a total
+    // heap limit: message metadata and the client's already-decoded pull 
batch are separate.
+    private static final long MAX_READ_BODY_BYTES = 10L * 1024 * 1024;
     private static final int MAX_PAGE_SIZE = 100;
     private static final int MAX_CONSECUTIVE_OFFSET_ILLEGAL = 3;
     private static final int MAX_REPORTED_RESEND_FAILURES = 100;
@@ -205,7 +208,8 @@ public class RocketMQDLQProvider implements DLQProvider {
 
         DeadLetterScanResult scanResult;
         try {
-            scanResult = collectDeadLetters(instanceId, dlqTopic, begin, end, 
RESEND_HARD_CAP);
+            scanResult = collectDeadLetters(
+                    instanceId, dlqTopic, begin, end, RESEND_HARD_CAP, 
Long.MAX_VALUE);
         } catch (BusinessException e) {
             String detail = String.format("instanceId=%s, group=%s, 
dlqTopic=%s, targetTopic=%s, "
                             + "matched=0, resent=0, failed=0, 
scanIncomplete=true, scanFailedQueues=all",
@@ -336,14 +340,13 @@ public class RocketMQDLQProvider implements DLQProvider {
         if (begin >= end) {
             throw new BusinessException(400, "DLQ detail start time must be 
before end time");
         }
-        List<DLQMessageVO> all = collectDeadLetters(instanceId, dlqTopic, 
begin, end, RESEND_HARD_CAP)
-                .messages().stream()
-                .map(this::toExportVO)
-                .toList();
+        List<MessageExt> all = collectDeadLetters(
+                instanceId, dlqTopic, begin, end, RESEND_HARD_CAP, 
MAX_READ_BODY_BYTES).messages();
         long offset = Pagination.pageOffset(page, pageSize);
         int from = (int) Math.min(offset, all.size());
         int to = (int) Math.min(offset + pageSize, all.size());
-        return PageResult.of(all.subList(from, to), all.size(), page, 
pageSize);
+        List<DLQMessageVO> items = all.subList(from, 
to).stream().map(this::toExportVO).toList();
+        return PageResult.of(items, all.size(), page, pageSize);
     }
 
     @Override
@@ -356,7 +359,8 @@ public class RocketMQDLQProvider implements DLQProvider {
         String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + groupName;
         long end = endTime != null ? endTime : System.currentTimeMillis();
         long begin = startTime != null ? startTime : end - ONE_HOUR_MILLIS;
-        DeadLetterScanResult scanResult = collectDeadLetters(instanceId, 
dlqTopic, begin, end, RESEND_HARD_CAP);
+        DeadLetterScanResult scanResult = collectDeadLetters(
+                instanceId, dlqTopic, begin, end, RESEND_HARD_CAP, 
MAX_READ_BODY_BYTES);
         Set<String> selected = msgIds == null ? Collections.emptySet()
                 : new java.util.HashSet<>(msgIds);
         List<DLQMessageVO> messages = scanResult.messages().stream()
@@ -389,7 +393,8 @@ public class RocketMQDLQProvider implements DLQProvider {
         long end = endTime != null ? endTime : System.currentTimeMillis();
         long begin = startTime != null ? startTime : end - ONE_HOUR_MILLIS;
         int cap = maxCount == null || maxCount <= 0 ? RESEND_HARD_CAP : 
Math.min(maxCount, RESEND_HARD_CAP);
-        DeadLetterScanResult scanResult = collectDeadLetters(instanceId, 
dlqTopic, begin, end, cap);
+        DeadLetterScanResult scanResult = collectDeadLetters(
+                instanceId, dlqTopic, begin, end, cap, MAX_READ_BODY_BYTES);
         return DLQExportResultVO.builder()
                 
.messages(scanResult.messages().stream().map(this::toExportVO).toList())
                 .truncated(scanResult.truncated())
@@ -436,14 +441,15 @@ public class RocketMQDLQProvider implements DLQProvider {
     }
 
     private DeadLetterScanResult collectDeadLetters(String instanceId, String 
dlqTopic,
-                                                     long begin, long end, int 
cap) {
+                                                     long begin, long end, int 
cap, long bodyByteBudget) {
         return runtimeAdminClientResolver.executePullConsumer(instanceId,
-                consumer -> scanDeadLetters(consumer, dlqTopic, begin, end, 
cap));
+                consumer -> scanDeadLetters(consumer, dlqTopic, begin, end, 
cap, bodyByteBudget));
     }
 
     private DeadLetterScanResult scanDeadLetters(DefaultMQPullConsumer 
consumer, String dlqTopic,
-                                                  long begin, long end, int 
cap) {
+                                                  long begin, long end, int 
cap, long bodyByteBudget) {
         List<MessageExt> result = new ArrayList<>();
+        long retainedBodyBytes = 0;
         int failedQueueCount = 0;
         boolean truncated = false;
         try {
@@ -511,6 +517,12 @@ public class RocketMQDLQProvider implements DLQProvider {
                         for (MessageExt messageExt : 
pullResult.getMsgFoundList()) {
                             if (messageExt.getStoreTimestamp() >= begin
                                     && messageExt.getStoreTimestamp() <= end) {
+                                byte[] body = messageExt.getBody();
+                                int bodyBytes = body == null ? 0 : body.length;
+                                if (bodyBytes > bodyByteBudget - 
retainedBodyBytes) {
+                                    throw new ReadBodyLimitExceededException();
+                                }
+                                retainedBodyBytes += bodyBytes;
                                 result.add(messageExt);
                                 if (result.size() >= cap) {
                                     truncated = true;
@@ -519,6 +531,10 @@ public class RocketMQDLQProvider implements DLQProvider {
                             }
                         }
                     }
+                } catch (ReadBodyLimitExceededException e) {
+                    // A read limit is fatal for the whole request, not a 
failed queue that can
+                    // be skipped while returning a partial export as if it 
had succeeded.
+                    throw e;
                 } catch (Exception e) {
                     failedQueueCount++;
                     log.warn("Failed to scan DLQ queue {} in {}: {}", queue, 
dlqTopic, e.getMessage());
@@ -837,6 +853,12 @@ public class RocketMQDLQProvider implements DLQProvider {
         }
     }
 
+    private static final class ReadBodyLimitExceededException extends 
BusinessException {
+        private ReadBodyLimitExceededException() {
+            super(413, "DLQ read exceeds the 10 MiB message body limit; narrow 
the time range");
+        }
+    }
+
     private record DeadLetterScanResult(List<MessageExt> messages, int 
failedQueueCount, boolean truncated,
                                         boolean topicMissing) {
         boolean scanIncomplete() {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
index 9a8f8c483..94a252ed8 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
@@ -24,8 +24,11 @@ import org.springframework.context.annotation.Import;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
 import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
@@ -553,6 +556,35 @@ class DLQControllerTest extends WebMvcAuthTestSupport {
         verifyNoInteractions(dlqService);
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"messages", "export", "export-excel"})
+    void dlqReadShouldReturnPayloadTooLargeWhenBodyBudgetIsExceededTest(String 
operation) throws Exception {
+        BusinessException tooLarge = new BusinessException(413,
+                "DLQ read exceeds the 10 MiB message body limit; narrow the 
time range");
+        String path;
+        if ("messages".equals(operation)) {
+            path = "/api/dlq/test-group/messages";
+            when(dlqService.listMessages(eq("instance-1"), eq("test-group"), 
isNull(), isNull(), eq(1), eq(20)))
+                    .thenThrow(tooLarge);
+        } else if ("export".equals(operation)) {
+            path = "/api/dlq/export";
+            when(dlqService.exportMessages(eq("instance-1"), eq("test-group"), 
isNull(), isNull(), isNull()))
+                    .thenThrow(tooLarge);
+        } else {
+            path = "/api/dlq/export-excel";
+            when(dlqService.exportExcel(eq("instance-1"), eq("test-group"), 
isNull(), isNull(), isNull()))
+                    .thenThrow(tooLarge);
+        }
+
+        mockMvc.perform(get(path)
+                        .param("instanceId", "instance-1")
+                        .param("groupName", "test-group"))
+                .andExpect(status().isPayloadTooLarge())
+                
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
+                .andExpect(jsonPath("$.code").value(413))
+                .andExpect(jsonPath("$.message").value(tooLarge.getMessage()));
+    }
+
     @Test
     void exportDLQExcelShouldRejectBracketedSelectionTest() throws Exception {
         mockMvc.perform(get("/api/dlq/export-excel")
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQPayloadBudgetTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQPayloadBudgetTest.java
new file mode 100644
index 000000000..f395349ee
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQPayloadBudgetTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.studio.provider.apache;
+
+import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
+import org.apache.rocketmq.client.consumer.PullResult;
+import org.apache.rocketmq.client.consumer.PullStatus;
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.client.producer.SendStatus;
+import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.common.message.Message;
+import org.apache.rocketmq.common.message.MessageConst;
+import org.apache.rocketmq.common.message.MessageAccessor;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.apache.rocketmq.common.message.MessageQueue;
+import org.apache.rocketmq.studio.cluster.broker.MqClientPool;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.dlq.DLQExportResultVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQMessageVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQResendResultVO;
+import org.apache.rocketmq.studio.ops.audit.AuditService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RocketMQDLQPayloadBudgetTest {
+
+    private static final String DLQ_TOPIC = MixAll.DLQ_GROUP_TOPIC_PREFIX + 
"group-a";
+    private static final int BODY_BUDGET = 10 * 1024 * 1024;
+    private static final int HALF_BUDGET = BODY_BUDGET / 2;
+
+    @Mock
+    private RuntimeAdminClientResolver resolver;
+    @Mock
+    private DefaultMQPullConsumer consumer;
+    @Mock
+    private DefaultMQProducer producer;
+    @Mock
+    private AuditService auditService;
+
+    private RocketMQDLQProvider provider;
+
+    @BeforeEach
+    void setUp() {
+        when(resolver.executePullConsumer(eq("instance-a"), 
any())).thenAnswer(invocation -> {
+            MqClientPool.ClientAction<DefaultMQPullConsumer, Object> action = 
invocation.getArgument(1);
+            return action.apply(consumer);
+        });
+        provider = new RocketMQDLQProvider(resolver, auditService);
+    }
+
+    @ParameterizedTest
+    @EnumSource(ReadOperation.class)
+    void 
readRejectsAggregateBodyOverflowBeforeConvertingMessagesTest(ReadOperation 
operation) throws Exception {
+        MessageExt first = spy(message("first", new byte[HALF_BUDGET]));
+        MessageExt second = spy(message("second", new byte[HALF_BUDGET + 1]));
+        stubMessages(List.of(first, second));
+
+        assertReadTooLarge(operation);
+
+        verify(first, never()).getProperties();
+        verify(second, never()).getProperties();
+        verifyNoInteractions(producer, auditService);
+    }
+
+    @Test
+    void readCountsBodiesAcrossPullBatchesTest() throws Exception {
+        MessageQueue queue = new MessageQueue(DLQ_TOPIC, "broker-a", 0);
+        
when(consumer.fetchSubscribeMessageQueues(DLQ_TOPIC)).thenReturn(Set.of(queue));
+        stubWindow(queue, 3L);
+        when(consumer.pull(queue, "*", 0L, 32)).thenReturn(found(1L,
+                List.of(message("first", new byte[HALF_BUDGET]))));
+        when(consumer.pull(queue, "*", 1L, 32)).thenReturn(found(2L,
+                List.of(message("second", new byte[HALF_BUDGET + 1]))));
+
+        assertReadTooLarge(ReadOperation.JSON);
+
+        verify(consumer, never()).pull(queue, "*", 2L, 32);
+    }
+
+    @Test
+    void 
readCountsBodiesAcrossQueuesAndDoesNotReturnPartialSuccessOnOverflowTest() 
throws Exception {
+        MessageQueue first = new MessageQueue(DLQ_TOPIC, "broker-a", 0);
+        MessageQueue unavailable = new MessageQueue(DLQ_TOPIC, "broker-b", 0);
+        MessageQueue last = new MessageQueue(DLQ_TOPIC, "broker-c", 0);
+        when(consumer.fetchSubscribeMessageQueues(DLQ_TOPIC))
+                .thenReturn(new LinkedHashSet<>(List.of(first, unavailable, 
last)));
+        stubWindow(first, 1L);
+        when(consumer.searchOffset(unavailable, 100L)).thenThrow(new 
BusinessException(502, "Unavailable"));
+        stubWindow(last, 1L);
+        when(consumer.pull(first, "*", 0L, 32)).thenReturn(found(1L,
+                List.of(message("first", new byte[HALF_BUDGET]))));
+        when(consumer.pull(last, "*", 0L, 32)).thenReturn(found(1L,
+                List.of(message("last", new byte[HALF_BUDGET + 1]))));
+
+        assertReadTooLarge(ReadOperation.JSON);
+    }
+
+    @Test
+    void exportPreservesCompleteBodiesAtExactBudgetTest() throws Exception {
+        stubMessages(List.of(message("first", new byte[HALF_BUDGET]),
+                message("second", new byte[HALF_BUDGET])));
+
+        DLQExportResultVO result = provider.exportMessages("instance-a", 
"group-a", 100L, 200L, 1000);
+
+        assertThat(result.isTruncated()).isFalse();
+        assertThat(result.getFailedQueueCount()).isZero();
+        assertThat(result.getMessages()).hasSize(2).allSatisfy(message -> {
+            assertThat(message.getBody()).hasSize(HALF_BUDGET);
+            assertThat(message.getBodyBase64()).hasSize(4 * ((HALF_BUDGET + 2) 
/ 3));
+        });
+    }
+
+    @Test
+    void nullBodyDoesNotExceedAnExactlyExhaustedBudgetTest() throws Exception {
+        stubMessages(List.of(message("first", new byte[HALF_BUDGET]),
+                message("second", new byte[HALF_BUDGET]), message("empty", 
null)));
+
+        PageResult<DLQMessageVO> result = provider.listMessages("instance-a", 
"group-a", 100L, 200L, 3, 1);
+
+        assertThat(result.getTotal()).isEqualTo(3);
+        assertThat(result.getItems()).singleElement().satisfies(message -> {
+            assertThat(message.getMsgId()).isEqualTo("empty");
+            assertThat(message.getBody()).isNull();
+            assertThat(message.getBodyBase64()).isNull();
+        });
+    }
+
+    @Test
+    void listConvertsOnlyTheRequestedPageTest() throws Exception {
+        MessageExt first = spy(message("first", new byte[] {1}));
+        MessageExt second = spy(message("second", new byte[] {2}));
+        MessageExt third = spy(message("third", new byte[] {3}));
+        stubMessages(List.of(first, second, third));
+
+        PageResult<DLQMessageVO> result = provider.listMessages("instance-a", 
"group-a", 100L, 200L, 2, 1);
+
+        assertThat(result.getTotal()).isEqualTo(3);
+        assertThat(result.getPage()).isEqualTo(2);
+        assertThat(result.getSize()).isEqualTo(1);
+        assertThat(result.getItems()).singleElement().satisfies(message -> {
+            assertThat(message.getMsgId()).isEqualTo("second");
+            assertThat(message.getBodyBase64()).isEqualTo("Ag==");
+        });
+        for (MessageExt outsidePage : List.of(first, third)) {
+            verify(outsidePage).getBody(); // Budget accounting only, without 
UTF-8/Base64 conversion.
+            verify(outsidePage, never()).getProperties();
+            verify(outsidePage, never()).getMsgId();
+        }
+    }
+
+    @Test
+    void listBeyondTheLastPageDoesNotConvertMessagesTest() throws Exception {
+        MessageExt message = spy(message("first", new byte[] {1}));
+        stubMessages(List.of(message));
+
+        PageResult<DLQMessageVO> result = provider.listMessages(
+                "instance-a", "group-a", 100L, 200L, Integer.MAX_VALUE, 100);
+
+        assertThat(result.getTotal()).isEqualTo(1);
+        assertThat(result.getItems()).isEmpty();
+        verify(message).getBody();
+        verify(message, never()).getProperties();
+    }
+
+    @Test
+    void readDoesNotChargeBodiesOutsideTheRequestedTimeWindowTest() throws 
Exception {
+        MessageExt outsideWindow = message("outside", new byte[BODY_BUDGET + 
1]);
+        outsideWindow.setStoreTimestamp(99L);
+        stubMessages(List.of(outsideWindow, message("inside", new byte[] 
{1})));
+
+        DLQExportResultVO result = provider.exportMessages("instance-a", 
"group-a", 100L, 200L, 1000);
+
+        
assertThat(result.getMessages()).extracting(DLQMessageVO::getMsgId).containsExactly("inside");
+        assertThat(result.isTruncated()).isFalse();
+    }
+
+    @Test
+    void exportKeepsTheRequestedCountCapBeforeConsideringFurtherBodiesTest() 
throws Exception {
+        stubMessages(List.of(message("first", new byte[] {1}),
+                message("unneeded", new byte[BODY_BUDGET + 1])));
+
+        DLQExportResultVO result = provider.exportMessages("instance-a", 
"group-a", 100L, 200L, 1);
+
+        
assertThat(result.getMessages()).extracting(DLQMessageVO::getMsgId).containsExactly("first");
+        assertThat(result.getLimit()).isEqualTo(1);
+        assertThat(result.isTruncated()).isTrue();
+        assertThat(result.getFailedQueueCount()).isZero();
+    }
+
+    @Test
+    void resendKeepsBodiesAboveTheReadBudgetUnchangedTest() throws Exception {
+        MessageExt first = message("first", new byte[HALF_BUDGET]);
+        MessageExt second = message("second", new byte[HALF_BUDGET + 1]);
+        MessageAccessor.setProperties(first, 
Map.of(MessageConst.PROPERTY_RETRY_TOPIC, "orders"));
+        MessageAccessor.setProperties(second, 
Map.of(MessageConst.PROPERTY_RETRY_TOPIC, "orders"));
+        stubMessages(List.of(first, second));
+        when(resolver.executeProducer(eq("instance-a"), 
any())).thenAnswer(invocation -> {
+            MqClientPool.ClientAction<DefaultMQProducer, Object> action = 
invocation.getArgument(1);
+            return action.apply(producer);
+        });
+        SendResult accepted = new SendResult();
+        accepted.setSendStatus(SendStatus.SEND_OK);
+        when(producer.send(any(Message.class))).thenReturn(accepted);
+
+        DLQResendResultVO result = provider.resendMessages("instance-a", 
"group-a", 100L, 200L, null);
+
+        assertThat(result.getMatched()).isEqualTo(2);
+        assertThat(result.getResent()).isEqualTo(2);
+        assertThat(result.getOutcome()).isEqualTo("SUCCESS");
+        ArgumentCaptor<Message> sent = ArgumentCaptor.forClass(Message.class);
+        verify(producer, times(2)).send(sent.capture());
+        
assertThat(sent.getAllValues().get(0).getBody()).isSameAs(first.getBody());
+        
assertThat(sent.getAllValues().get(1).getBody()).isSameAs(second.getBody());
+    }
+
+    private void assertReadTooLarge(ReadOperation operation) {
+        assertThatThrownBy(() -> {
+            switch (operation) {
+                case LIST -> provider.listMessages("instance-a", "group-a", 
100L, 200L, 1, 1);
+                case JSON -> provider.exportMessages("instance-a", "group-a", 
100L, 200L, 1000);
+                case EXCEL -> provider.exportExcel("instance-a", "group-a", 
100L, 200L, null);
+                case SELECTED_EXCEL -> provider.exportExcel(
+                        "instance-a", "group-a", 100L, 200L, List.of("first"));
+            }
+        }).isInstanceOfSatisfying(BusinessException.class, exception -> {
+            assertThat(exception.getCode()).isEqualTo(413);
+            assertThat(exception.getMessage()).contains("10 MiB", "narrow the 
time range");
+        });
+    }
+
+    private void stubMessages(List<MessageExt> messages) throws Exception {
+        MessageQueue queue = new MessageQueue(DLQ_TOPIC, "broker-a", 0);
+        
when(consumer.fetchSubscribeMessageQueues(DLQ_TOPIC)).thenReturn(Set.of(queue));
+        stubWindow(queue, messages.size());
+        when(consumer.pull(queue, "*", 0L, 
32)).thenReturn(found(messages.size(), messages));
+    }
+
+    private void stubWindow(MessageQueue queue, long endOffset) throws 
Exception {
+        when(consumer.searchOffset(queue, 100L)).thenReturn(0L);
+        when(consumer.searchOffset(queue, 201L)).thenReturn(endOffset);
+    }
+
+    private static PullResult found(long nextOffset, List<MessageExt> 
messages) {
+        return new PullResult(PullStatus.FOUND, nextOffset, 0L, nextOffset, 
messages);
+    }
+
+    private static MessageExt message(String msgId, byte[] body) {
+        MessageExt message = new MessageExt();
+        message.setMsgId(msgId);
+        message.setTopic(DLQ_TOPIC);
+        message.setStoreTimestamp(150L);
+        message.setBody(body);
+        return message;
+    }
+
+    private enum ReadOperation {
+        LIST, JSON, EXCEL, SELECTED_EXCEL
+    }
+}

Reply via email to