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 809fdae7 fix(provider): handle empty cloud catalog responses (#2041)
809fdae7 is described below
commit 809fdae72ef297754001d0959c11a782edf38f7f
Author: xdz997 <[email protected]>
AuthorDate: Fri Aug 14 10:31:49 2026 +0800
fix(provider): handle empty cloud catalog responses (#2041)
Consolidates #1815 and #2041: return an empty trace instead of a 404 when
Aliyun trace queries have no data, and prefer TotalCount when Tencent
countTopics receives an empty data page.
---
.../provider/alibaba/AliyunInstanceProvider.java | 10 +++-
.../provider/tencent/TencentInstanceProvider.java | 16 +++---
.../alibaba/AliyunInstanceProviderTest.java | 44 ++++++++++++++++
.../tencent/TencentInstanceProviderTest.java | 58 ++++++++++++++++++++++
4 files changed, 121 insertions(+), 7 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
index 56c38b37..6d0d0556 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
@@ -61,6 +61,7 @@ import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -441,11 +442,18 @@ public class AliyunInstanceProvider implements
InstanceProvider {
GetTraceResponseBody body = response == null ? null :
response.getBody();
GetTraceResponseBody.Data data = body == null ? null : body.getData();
if (data == null) {
- throw new BusinessException(404, "Message trace not found: " +
msgId);
+ return emptyTraceRecord();
}
return AliyunConverters.toTraceRecord(data);
}
+ private static TraceRecordVO emptyTraceRecord() {
+ return TraceRecordVO.builder()
+ .nodes(Collections.emptyList())
+ .consumerStatus(Collections.emptyList())
+ .build();
+ }
+
private Context resolve(String instanceId) {
if (!StringUtils.hasText(instanceId)) {
throw new BusinessException(400, "instanceId is required");
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 6e38ce1a..a70c8fe4 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
@@ -132,18 +132,22 @@ public class TencentInstanceProvider implements
InstanceProvider {
request.setLimit(1L);
DescribeTopicListResponse response =
clientFactory.call(context.credentialId(), context.regionId(),
client -> client.DescribeTopicList(request));
- if (response == null || response.getData() == null ||
response.getData().length == 0) {
+ if (response == null) {
return 0;
}
- // Use the response total count if available; otherwise fall back to
full scan
+ // TotalCount is independent of the current page contents and remains
authoritative
+ // when Tencent returns an empty Data array for the minimal count
request.
Long total = response.getTotalCount();
- if (total == null) {
- return listTopics(instanceId, null, null, false).size();
+ if (total != null) {
+ if (total <= 0L) {
+ return 0;
+ }
+ return total > Integer.MAX_VALUE ? Integer.MAX_VALUE :
total.intValue();
}
- if (total <= 0L) {
+ if (response.getData() == null || response.getData().length == 0) {
return 0;
}
- return total > Integer.MAX_VALUE ? Integer.MAX_VALUE :
total.intValue();
+ return listTopics(instanceId, null, null, false).size();
}
@Override
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
index d39d0a6f..961a89c8 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
@@ -360,6 +360,50 @@ class AliyunInstanceProviderTest {
assertThat(consumer.getStatus()).isEqualTo("CONSUME_OK");
}
+ @Test
+ void getMessageTraceShouldReturnEmptyTraceWhenAliyunDataIsNullTest() {
+ stubInstance();
+ stubCallThrough();
+ GetTraceResponse response = GetTraceResponse.create().toBuilder()
+ .statusCode(200)
+ .body(GetTraceResponseBody.builder().data(null).build())
+ .build();
+
when(asyncClient.getTrace(any())).thenReturn(CompletableFuture.completedFuture(response));
+
+ TraceRecordVO trace = provider.getMessageTrace(STUDIO_INSTANCE_ID,
"msg-without-trace", "orders");
+
+ assertThat(trace.getNodes()).isEmpty();
+ assertThat(trace.getConsumerStatus()).isEmpty();
+ }
+
+ @Test
+ void getMessageTraceShouldReturnEmptyTraceWhenAliyunBodyIsNullTest() {
+ stubInstance();
+ stubCallThrough();
+ GetTraceResponse response = GetTraceResponse.create().toBuilder()
+ .statusCode(200)
+ .body(null)
+ .build();
+
when(asyncClient.getTrace(any())).thenReturn(CompletableFuture.completedFuture(response));
+
+ TraceRecordVO trace = provider.getMessageTrace(STUDIO_INSTANCE_ID,
"msg-without-body", "orders");
+
+ assertThat(trace.getNodes()).isEmpty();
+ assertThat(trace.getConsumerStatus()).isEmpty();
+ }
+
+ @Test
+ void getMessageTraceShouldReturnEmptyTraceWhenAliyunResponseIsNullTest() {
+ stubInstance();
+ stubCallThrough();
+
when(asyncClient.getTrace(any())).thenReturn(CompletableFuture.completedFuture(null));
+
+ TraceRecordVO trace = provider.getMessageTrace(STUDIO_INSTANCE_ID,
"msg-without-response", "orders");
+
+ assertThat(trace.getNodes()).isEmpty();
+ assertThat(trace.getConsumerStatus()).isEmpty();
+ }
+
@Test
void mappedBusinessExceptionShouldPropagateTest() {
stubInstance();
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 f0459255..67b83c70 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
@@ -32,6 +32,7 @@ import
com.tencentcloudapi.trocket.v20230308.models.MessageItem;
import com.tencentcloudapi.trocket.v20230308.models.MessageTraceItem;
import
com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListByGroupResponse;
import
com.tencentcloudapi.trocket.v20230308.models.DescribeConsumerGroupListRequest;
+import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListRequest;
import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListResponse;
import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicRequest;
import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicResponse;
@@ -121,6 +122,35 @@ class TencentInstanceProviderTest {
assertThat(provider.countTopics(STUDIO_INSTANCE_ID)).isEqualTo(Integer.MAX_VALUE);
}
+ @Test
+ void countTopicsShouldUseTotalCountFromSingleItemRequestTest() throws
Exception {
+ DescribeTopicListResponse response = new DescribeTopicListResponse();
+ response.setTotalCount(501L);
+ response.setData(new TopicItem[]{topicItem("orders", "NORMAL", 8L)});
+ when(client.DescribeTopicList(any())).thenReturn(response);
+
+ int count = provider.countTopics(STUDIO_INSTANCE_ID);
+
+ assertThat(count).isEqualTo(501);
+ ArgumentCaptor<DescribeTopicListRequest> captor =
+ ArgumentCaptor.forClass(DescribeTopicListRequest.class);
+ verify(client).DescribeTopicList(captor.capture());
+
assertThat(captor.getValue().getInstanceId()).isEqualTo(CLOUD_INSTANCE_ID);
+ assertThat(captor.getValue().getOffset()).isZero();
+ assertThat(captor.getValue().getLimit()).isEqualTo(1L);
+ }
+
+ @Test
+ void countTopicsShouldUseTotalCountEvenWhenDataIsEmptyTest() throws
Exception {
+ DescribeTopicListResponse response = new DescribeTopicListResponse();
+ response.setTotalCount(501L);
+ response.setData(new TopicItem[0]);
+ when(client.DescribeTopicList(any())).thenReturn(response);
+
+ assertThat(provider.countTopics(STUDIO_INSTANCE_ID)).isEqualTo(501);
+ verify(client, times(1)).DescribeTopicList(any());
+ }
+
@Test
void listTopicsShouldMapAndFilterAndEnrichTimesTest() throws Exception {
TopicItem normal = topicItem("orders", "NORMAL", 8L);
@@ -355,6 +385,34 @@ class TencentInstanceProviderTest {
.containsOnly(100L);
}
+ @Test
+ void
getTopicConsumersShouldStopAfterSubscriptionCountReachedOnFullPageTest() throws
Exception {
+ DescribeTopicResponse firstPage = new DescribeTopicResponse();
+ firstPage.setSubscriptionCount(200L);
+ firstPage.setSubscriptionData(IntStream.range(0, 100)
+ .mapToObj(index -> subscription("GID_" + index))
+ .toArray(SubscriptionData[]::new));
+ DescribeTopicResponse secondPage = new DescribeTopicResponse();
+ secondPage.setSubscriptionCount(200L);
+ secondPage.setSubscriptionData(IntStream.range(100, 200)
+ .mapToObj(index -> subscription("GID_" + index))
+ .toArray(SubscriptionData[]::new));
+ when(client.DescribeTopic(any())).thenReturn(firstPage, secondPage);
+
+ List<TopicConsumerVO> consumers =
provider.getTopicConsumers(STUDIO_INSTANCE_ID, "orders");
+
+ assertThat(consumers).hasSize(200);
+ assertThat(consumers.get(199).getGroup()).isEqualTo("GID_199");
+ ArgumentCaptor<DescribeTopicRequest> captor =
ArgumentCaptor.forClass(DescribeTopicRequest.class);
+ verify(client, times(2)).DescribeTopic(captor.capture());
+ assertThat(captor.getAllValues())
+ .extracting(DescribeTopicRequest::getOffset)
+ .containsExactly(0L, 100L);
+ assertThat(captor.getAllValues())
+ .extracting(DescribeTopicRequest::getLimit)
+ .containsOnly(100L);
+ }
+
@Test
void listConsumerGroupsShouldClampOversizedRetryCounts() throws Exception {
ConsumeGroupItem item = new ConsumeGroupItem();