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 -> {