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 34b033208 fix(tencent): reject incomplete resource pages (#4531)
34b033208 is described below

commit 34b033208e7aa6171969d5e13c5bb2d352285a9c
Author: zmuxuny <[email protected]>
AuthorDate: Mon Sep 21 21:00:25 2026 +0800

    fix(tencent): reject incomplete resource pages (#4531)
    
    `TencentInstanceProvider` ended three paged resource scans — the topic 
list, the consumer group list and a group's subscriptions — as soon as a page 
came back empty or short, and `hasFetchedAll` compared the requested 
`PAGE_SIZE` against `TotalCount` rather than the rows the response actually 
carried. A short page that still left `offset + returned` below a known 
`TotalCount` therefore closed the loop silently, and Studio rendered a 
truncated topic or group inventory as if it were complete.
    
    Each page now goes through `requireCompletePage`, which throws 
`BusinessException(502)` when a non-negative `TotalCount` contradicts a short 
page, and `hasFetchedAll` counts returned rows. A missing or negative 
`TotalCount` keeps the previous short-page termination, so a genuinely empty 
catalog still resolves to an empty list.
    
    Fixes #4530
---
 .../provider/tencent/TencentInstanceProvider.java  | 42 +++++++++++++++-------
 .../tencent/TencentInstanceProviderTest.java       | 38 ++++++++++++++++++++
 2 files changed, 68 insertions(+), 12 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
index 9685c0c99..ee08031e8 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
@@ -222,11 +222,15 @@ public class TencentInstanceProvider implements 
InstanceProvider {
         for (long offset = 0L; ; offset += PAGE_SIZE) {
             DescribeTopicListResponse response = describeTopics(context, type, 
search, offset, PAGE_SIZE);
             TopicItem[] data = response == null ? null : response.getData();
-            if (data == null || data.length == 0) {
+            Long totalCount = response == null ? null : 
response.getTotalCount();
+            int returned = data == null ? 0 : data.length;
+            requireCompletePage("topic", offset, returned, totalCount);
+            if (returned == 0) {
                 break;
             }
             topics.addAll(toTopics(data, instanceId, context, enrichTimes));
-            if (hasFetchedAll(offset, PAGE_SIZE, response.getTotalCount()) || 
data.length < PAGE_SIZE) {
+            if (hasFetchedAll(offset, returned, totalCount)
+                    || isUnknownTotalCount(totalCount) && returned < 
PAGE_SIZE) {
                 break;
             }
         }
@@ -280,12 +284,20 @@ public class TencentInstanceProvider implements 
InstanceProvider {
         return topics;
     }
 
-    private static boolean hasFetchedAll(long offset, int pageSize, Long 
totalCount) {
-        return totalCount != null && totalCount >= 0L && offset + pageSize >= 
totalCount;
+    private static void requireCompletePage(String resource, long offset, int 
returned, Long totalCount) {
+        if (totalCount != null && totalCount >= 0L
+                && returned < PAGE_SIZE && offset + returned < totalCount) {
+            throw new BusinessException(502,
+                    "Tencent Cloud returned an incomplete " + resource + " 
page");
+        }
+    }
+
+    private static boolean hasFetchedAll(long offset, int returned, Long 
totalCount) {
+        return totalCount != null && totalCount >= 0L && offset + returned >= 
totalCount;
     }
 
-    private static boolean hasFetchedAll(long fetched, Long totalCount) {
-        return totalCount != null && totalCount >= 0L && fetched >= totalCount;
+    private static boolean isUnknownTotalCount(Long totalCount) {
+        return totalCount == null || totalCount < 0L;
     }
 
     private static PageResult<TopicVO> paginate(List<TopicVO> topics, int 
page, int pageSize) {
@@ -429,7 +441,10 @@ public class TencentInstanceProvider implements 
InstanceProvider {
             DescribeConsumerGroupListResponse response = 
clientFactory.call(context.credentialId(), context.regionId(),
                     client -> client.DescribeConsumerGroupList(request));
             ConsumeGroupItem[] data = response == null ? null : 
response.getData();
-            if (data == null || data.length == 0) {
+            Long totalCount = response == null ? null : 
response.getTotalCount();
+            int returned = data == null ? 0 : data.length;
+            requireCompletePage("consumer group", offset, returned, 
totalCount);
+            if (returned == 0) {
                 break;
             }
             for (ConsumeGroupItem item : data) {
@@ -444,7 +459,8 @@ public class TencentInstanceProvider implements 
InstanceProvider {
                     groups.add(group);
                 }
             }
-            if (data.length < PAGE_SIZE || hasFetchedAll(offset, PAGE_SIZE, 
response.getTotalCount())) {
+            if (hasFetchedAll(offset, returned, totalCount)
+                    || isUnknownTotalCount(totalCount) && returned < 
PAGE_SIZE) {
                 break;
             }
         }
@@ -987,7 +1003,6 @@ public class TencentInstanceProvider implements 
InstanceProvider {
 
     private List<SubscriptionData> listTopicSubscriptionsByGroup(Context 
context, String groupName) {
         List<SubscriptionData> all = new ArrayList<>();
-        long fetched = 0L;
         for (long offset = 0L; ; offset += PAGE_SIZE) {
             DescribeTopicListByGroupRequest request = new 
DescribeTopicListByGroupRequest();
             request.setInstanceId(context.cloudInstanceId());
@@ -997,12 +1012,15 @@ public class TencentInstanceProvider implements 
InstanceProvider {
             DescribeTopicListByGroupResponse response = 
clientFactory.call(context.credentialId(), context.regionId(),
                     client -> client.DescribeTopicListByGroup(request));
             SubscriptionData[] data = response == null ? null : 
response.getData();
-            if (data == null || data.length == 0) {
+            Long totalCount = response == null ? null : 
response.getTotalCount();
+            int returned = data == null ? 0 : data.length;
+            requireCompletePage("consumer group subscription", offset, 
returned, totalCount);
+            if (returned == 0) {
                 break;
             }
-            fetched += data.length;
             all.addAll(Arrays.asList(data));
-            if (data.length < PAGE_SIZE || hasFetchedAll(fetched, 
response.getTotalCount())) {
+            if (hasFetchedAll(offset, returned, totalCount)
+                    || isUnknownTotalCount(totalCount) && returned < 
PAGE_SIZE) {
                 break;
             }
         }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
index 64e8a0932..9098b0e57 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
@@ -213,6 +213,18 @@ class TencentInstanceProviderTest {
         verify(client, times(1)).DescribeTopicList(any());
     }
 
+    @Test
+    void listTopicsShouldRejectIncompletePageWhenTotalCountRequiresMoreTest() 
throws Exception {
+        DescribeTopicListResponse response = new DescribeTopicListResponse();
+        response.setTotalCount(2L);
+        response.setData(new TopicItem[]{topicItem("orders", "NORMAL", 8L)});
+        when(client.DescribeTopicList(any())).thenReturn(response);
+
+        assertThatThrownBy(() -> provider.listTopics(STUDIO_INSTANCE_ID, null, 
null))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(502));
+    }
+
     @Test
     void listTopicsShouldMapAndFilterAndEnrichTimesTest() throws Exception {
         TopicItem normal = topicItem("orders", "NORMAL", 8L);
@@ -562,6 +574,20 @@ class TencentInstanceProviderTest {
                 .containsOnly(100L);
     }
 
+    @Test
+    void 
listConsumerGroupsShouldRejectIncompletePageWhenTotalCountRequiresMoreTest() 
throws Exception {
+        ConsumeGroupItem item = new ConsumeGroupItem();
+        item.setConsumerGroup("GID_partial");
+        DescribeConsumerGroupListResponse response = new 
DescribeConsumerGroupListResponse();
+        response.setTotalCount(2L);
+        response.setData(new ConsumeGroupItem[]{item});
+        when(client.DescribeConsumerGroupList(any())).thenReturn(response);
+
+        assertThatThrownBy(() -> 
provider.listConsumerGroups(STUDIO_INSTANCE_ID, null))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(502));
+    }
+
     @Test
     void listConsumerGroupsShouldClampOversizedRetryCounts() throws Exception {
         ConsumeGroupItem item = new ConsumeGroupItem();
@@ -704,6 +730,18 @@ class TencentInstanceProviderTest {
         
assertThat(preview.getQueues().get(0).getRiskLevel()).isEqualTo("WARNING");
     }
 
+    @Test
+    void 
getGroupSubscriptionsShouldRejectIncompletePageWhenTotalCountRequiresMoreTest() 
throws Exception {
+        DescribeTopicListByGroupResponse response = new 
DescribeTopicListByGroupResponse();
+        response.setTotalCount(2L);
+        response.setData(new SubscriptionData[]{subscription("orders")});
+        when(client.DescribeTopicListByGroup(any())).thenReturn(response);
+
+        assertThatThrownBy(() -> 
provider.getGroupSubscriptions(STUDIO_INSTANCE_ID, "GID_test"))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(502));
+    }
+
     @Test
     void 
getGroupSubscriptionsShouldFetchExactlyTenThousandTencentSubscriptionsTest() 
throws Exception {
         when(client.DescribeTopicListByGroup(any())).thenAnswer(invocation -> {

Reply via email to