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 0b131e631 [ISSUE #2902][ISSUE #2903][ISSUE #2911][ISSUE #2912][ISSUE 
#2913] fix(server): consolidate collection and registry robustness (#2907)
0b131e631 is described below

commit 0b131e631d7a440a11869a9f6a04543605caad33
Author: shown <[email protected]>
AuthorDate: Fri Sep 4 14:17:09 2026 +0800

    [ISSUE #2902][ISSUE #2903][ISSUE #2911][ISSUE #2912][ISSUE #2913] 
fix(server): consolidate collection and registry robustness (#2907)
    
    * [ISSUE #2902] Run NameServer registry probes concurrently
    
    Signed-off-by: yuluo-yx <[email protected]>
    
    * [ISSUE #2903] Stabilize Apache provider broker fallback
    
    * [ISSUE #2911] Fix Aliyun lag aggregation
    
    * [ISSUE #2912] Align system group filtering
    
    * [ISSUE #2913] Make metric snapshot batches atomic
    
    ---------
    
    Signed-off-by: yuluo-yx <[email protected]>
---
 .../studio/cluster/broker/RegistryProbeRunner.java |  4 +-
 .../MybatisPlusMetricSnapshotRepository.java       |  2 +
 .../studio/common/util/SystemGroupFilter.java      |  9 ++--
 .../studio/provider/alibaba/AliyunConverters.java  |  5 +-
 .../provider/apache/RocketMQClusterProvider.java   |  7 +--
 .../cluster/broker/RegistryProbeRunnerTest.java    | 44 ++++++++++++++++
 .../MybatisPlusMetricSnapshotRepositoryTest.java   | 10 ++++
 .../studio/common/util/SystemGroupFilterTest.java  | 12 +++--
 .../provider/alibaba/AliyunConvertersLagTest.java  | 60 ++++++++++++++++++++++
 .../apache/RocketMQClusterProviderTest.java        | 19 +++++++
 10 files changed, 159 insertions(+), 13 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RegistryProbeRunner.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RegistryProbeRunner.java
index 16be981a6..22ea8bc58 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RegistryProbeRunner.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RegistryProbeRunner.java
@@ -74,8 +74,10 @@ class RegistryProbeRunner implements AutoCloseable {
      * or failing entry contributes an empty result without affecting the 
others.
      */
     List<ClusterVO> probeAll(List<NameserverRegistryVO> entries, ProbeFunction 
function) {
-        return entries.stream()
+        List<CompletableFuture<List<ClusterVO>>> probes = entries.stream()
                 .map(entry -> probeOne(entry, function))
+                .toList();
+        return probes.stream()
                 .map(CompletableFuture::join)
                 .flatMap(List::stream)
                 .toList();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepository.java
index 8cdd35480..2acc62368 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepository.java
@@ -24,6 +24,7 @@ import lombok.RequiredArgsConstructor;
 import org.apache.rocketmq.studio.persistence.entity.RmqMetricSnapshot;
 import org.apache.rocketmq.studio.persistence.mapper.RmqMetricSnapshotMapper;
 import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
@@ -42,6 +43,7 @@ public class MybatisPlusMetricSnapshotRepository implements 
MetricSnapshotReposi
     private final ObjectMapper objectMapper;
 
     @Override
+    @Transactional
     public void saveAll(List<MetricSample> samples) {
         for (MetricSample sample : samples) {
             mapper.insert(toEntity(sample));
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/SystemGroupFilter.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/SystemGroupFilter.java
index d352ec827..680deacb6 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/SystemGroupFilter.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/SystemGroupFilter.java
@@ -16,6 +16,8 @@
  */
 package org.apache.rocketmq.studio.common.util;
 
+import org.apache.rocketmq.common.MixAll;
+
 /**
  * Shared utility for identifying RocketMQ system consumer groups.
  *
@@ -39,15 +41,12 @@ public final class SystemGroupFilter {
         if (group == null || group.isEmpty()) {
             return true;
         }
-        return group.startsWith("CID_RMQ_SYS_")
+        return MixAll.isSysConsumerGroupPullMessage(group)
                 || group.startsWith("CID_ONSAPI_")
                 || group.startsWith("CID_SYS_")
                 || group.startsWith("CID_HOUSEKEEPING")
                 || group.startsWith("rmq_sys_")
                 || group.startsWith("%RETRY%")
-                || group.startsWith("%DLQ%")
-                || group.startsWith("TOOLS_CONSUMER")
-                || group.startsWith("FILTERSRV_CONSUMER")
-                || group.startsWith("SELF_TEST_");
+                || group.startsWith("%DLQ%");
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
index 8fcfd06f2..fd52ed63b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
@@ -192,7 +192,10 @@ final class AliyunConverters {
             }
         }
         GetConsumerGroupLagResponseBody.TotalLag totalLag = data.getTotalLag();
-        if (totalLag != null && totalLag.getReadyCount() != null) {
+        // The aggregate repeats the per-topic counts. Keep it only as a 
fallback when
+        // the API does not expose the topic breakdown, otherwise callers that 
sum rows
+        // report the same lag twice.
+        if (rows.isEmpty() && totalLag != null && totalLag.getReadyCount() != 
null) {
             rows.add(QueueProgressVO.builder()
                     .broker("total")
                     .queueId(0)
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
index 8deca28eb..e6d12fbdf 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
@@ -210,9 +210,10 @@ public class RocketMQClusterProvider implements 
ClusterProvider {
             // Use master address (brokerId = 0) preferentially
             String masterAddr = brokerData.getBrokerAddrs().get(0L);
             if (!StringUtils.hasText(masterAddr)) {
-                masterAddr = brokerData.getBrokerAddrs().values().stream()
-                        .filter(StringUtils::hasText)
-                        .findFirst()
+                masterAddr = brokerData.getBrokerAddrs().entrySet().stream()
+                        .filter(entry -> StringUtils.hasText(entry.getValue()))
+                        .min(Map.Entry.comparingByKey())
+                        .map(Map.Entry::getValue)
                         .orElse(null);
             }
             if (!StringUtils.hasText(masterAddr)) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/RegistryProbeRunnerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/RegistryProbeRunnerTest.java
new file mode 100644
index 000000000..92054cf6e
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/RegistryProbeRunnerTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.broker;
+
+import org.apache.rocketmq.studio.cluster.nameserver.NameserverRegistryVO;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RegistryProbeRunnerTest {
+
+    @Test
+    void startsIndependentRegistryProbesConcurrently() throws Exception {
+        CountDownLatch probesStarted = new CountDownLatch(2);
+        CountDownLatch releaseProbes = new CountDownLatch(1);
+        List<NameserverRegistryVO> entries = List.of(
+                
NameserverRegistryVO.builder().name("registry-a").namesrvAddr("a:9876").build(),
+                
NameserverRegistryVO.builder().name("registry-b").namesrvAddr("b:9876").build());
+
+        try (RegistryProbeRunner runner = new RegistryProbeRunner(2, 2, 
2_000)) {
+            CompletableFuture<List<ClusterVO>> result = 
CompletableFuture.supplyAsync(() ->
+                    runner.probeAll(entries, entry -> {
+                        probesStarted.countDown();
+                        releaseProbes.await();
+                        return 
List.of(ClusterVO.builder().id(entry.getName()).build());
+                    }));
+
+            assertThat(probesStarted.await(1, TimeUnit.SECONDS)).isTrue();
+            releaseProbes.countDown();
+            assertThat(result.get(1, TimeUnit.SECONDS))
+                    .extracting(ClusterVO::getId)
+                    .containsExactly("registry-a", "registry-b");
+        }
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepositoryTest.java
index 7d89fa996..76a8a5023 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepositoryTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MybatisPlusMetricSnapshotRepositoryTest.java
@@ -27,7 +27,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.lang.reflect.Method;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
@@ -43,6 +45,14 @@ class MybatisPlusMetricSnapshotRepositoryTest {
     @Mock
     private RmqMetricSnapshotMapper mapper;
 
+    @Test
+    void saveAllShouldUseOneTransactionForTheWholeSampleBatchTest() throws 
Exception {
+        Method saveAll = MybatisPlusMetricSnapshotRepository.class
+                .getMethod("saveAll", List.class);
+
+        assertThat(saveAll.isAnnotationPresent(Transactional.class)).isTrue();
+    }
+
     @Test
     void nullClusterScopeShouldOnlyReadUnscopedSnapshotsTest() {
         when(mapper.selectList(any(Wrapper.class))).thenReturn(List.of());
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemGroupFilterTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemGroupFilterTest.java
index 7068d46ed..3324949c5 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemGroupFilterTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/SystemGroupFilterTest.java
@@ -33,10 +33,16 @@ class SystemGroupFilterTest {
         assertThat(SystemGroupFilter.isSystem("rmq_sys_TRACE_DATA")).isTrue();
         assertThat(SystemGroupFilter.isSystem("%RETRY%consumer-a")).isTrue();
         assertThat(SystemGroupFilter.isSystem("%DLQ%consumer-a")).isTrue();
-        
assertThat(SystemGroupFilter.isSystem("TOOLS_CONSUMER_monitor")).isTrue();
-        
assertThat(SystemGroupFilter.isSystem("FILTERSRV_CONSUMER_filter")).isTrue();
-        assertThat(SystemGroupFilter.isSystem("SELF_TEST_GROUP")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("DEFAULT_CONSUMER")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("TOOLS_CONSUMER")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("SCHEDULE_CONSUMER")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("FILTERSRV_CONSUMER")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("__MONITOR_CONSUMER")).isTrue();
+        assertThat(SystemGroupFilter.isSystem("SELF_TEST_C_GROUP")).isTrue();
 
         
assertThat(SystemGroupFilter.isSystem("order-service-consumer")).isFalse();
+        
assertThat(SystemGroupFilter.isSystem("TOOLS_CONSUMER_monitor")).isFalse();
+        
assertThat(SystemGroupFilter.isSystem("FILTERSRV_CONSUMER_filter")).isFalse();
+        assertThat(SystemGroupFilter.isSystem("SELF_TEST_GROUP")).isFalse();
     }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConvertersLagTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConvertersLagTest.java
new file mode 100644
index 000000000..9fd14f418
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConvertersLagTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.alibaba;
+
+import com.aliyun.sdk.service.rocketmq20220801.models.DataTopicLagMapValue;
+import 
com.aliyun.sdk.service.rocketmq20220801.models.GetConsumerGroupLagResponseBody;
+import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AliyunConvertersLagTest {
+
+    @Test
+    void topicBreakdownShouldNotBeCountedAgainAsAnAggregateRow() {
+        GetConsumerGroupLagResponseBody.Data data = 
GetConsumerGroupLagResponseBody.Data.builder()
+                .topicLagMap(Map.of(
+                        "orders", 
DataTopicLagMapValue.builder().readyCount(40L).build(),
+                        "payments", 
DataTopicLagMapValue.builder().readyCount(60L).build()))
+                
.totalLag(GetConsumerGroupLagResponseBody.TotalLag.builder().readyCount(100L).build())
+                .build();
+
+        List<QueueProgressVO> rows = 
AliyunConverters.toQueueProgressRows(data);
+
+        assertThat(rows).extracting(QueueProgressVO::getTopic)
+                .containsExactlyInAnyOrder("orders", "payments");
+        assertThat(rows).noneMatch(row -> "total".equals(row.getBroker()));
+        
assertThat(rows.stream().mapToLong(QueueProgressVO::getDiffTotal).sum()).isEqualTo(100L);
+    }
+
+    @Test
+    void aggregateShouldRemainAvailableWhenTopicBreakdownIsMissing() {
+        GetConsumerGroupLagResponseBody.Data data = 
GetConsumerGroupLagResponseBody.Data.builder()
+                
.totalLag(GetConsumerGroupLagResponseBody.TotalLag.builder().readyCount(100L).build())
+                .build();
+
+        assertThat(AliyunConverters.toQueueProgressRows(data)).singleElement()
+                .satisfies(row -> {
+                    assertThat(row.getBroker()).isEqualTo("total");
+                    assertThat(row.getDiffTotal()).isEqualTo(100L);
+                });
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
index 89ea41124..bdfe2a6f2 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
@@ -210,6 +210,25 @@ class RocketMQClusterProviderTest {
                 .isEqualTo("10.0.0.12:10911");
     }
 
+    @Test
+    void discoverClustersShouldUseLowestBrokerIdWhenMasterIsMissing() throws 
Exception {
+        DefaultMQAdminExt adminExt = mock(DefaultMQAdminExt.class);
+        RocketMQClusterProvider provider = newProvider(adminExt);
+        ClusterInfo info = clusterInfo();
+        HashMap<Long, String> addrs = new HashMap<>();
+        addrs.put(32L, "10.0.0.32:10911");
+        addrs.put(1L, "10.0.0.1:10911");
+        info.getBrokerAddrTable().put(
+                "broker-a", new BrokerData("DefaultCluster", "broker-a", 
addrs));
+        when(adminExt.examineBrokerClusterInfo()).thenReturn(info);
+
+        ClusterVO cluster = provider.discoverClusters().get(0);
+
+        assertThat(cluster.getBrokers()).singleElement()
+                .extracting(broker -> broker.getAddr())
+                .isEqualTo("10.0.0.1:10911");
+    }
+
     @Test
     void discoverClustersShouldDiscoverProxiesViaHeartbeatSyncerTest() throws 
Exception {
         DefaultMQAdminExt adminExt = mock(DefaultMQAdminExt.class);

Reply via email to