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 7f97f3070 fix(metrics): avoid deriving queue lag from cloud aggregates 
(#6184)
7f97f3070 is described below

commit 7f97f3070b93855e8c1477435db1a11b6491434c
Author: zmuxuny <[email protected]>
AuthorDate: Mon Oct 12 00:17:32 2026 -0700

    fix(metrics): avoid deriving queue lag from cloud aggregates (#6184)
    
    `CloudRocketMqBusinessMetricsCollector.java:91-96` published 
`max(diffTotal)` over the provider's progress rows as an AVAILABLE 
`consumer.lag.max_queue`. Those rows are subscription-level aggregates rather 
than queues: the Aliyun and Tencent converters fill 
`brokerOffset`/`consumerOffset` with `QueueProgressVO.UNKNOWN_OFFSET` 
(`AliyunConverters.java:212-213` and `:226-227`, 
`TencentInstanceProvider.java:548-549`) and put the topic-level lag into 
`diffTotal`. A topic aggregate of 80 th [...]
    
    Six tests plus two parameterized vendor instances cover the change, driving 
the real evaluator and state machine rather than mocks. Reverting to AVAILABLE 
makes `doesNotPublishTopicTotalsAsQueueMaximumTest` evaluate 80 > 50 as met and 
makes `unsupportedQueueMaximumDoesNotClearAnActiveAlertTest` lose its `NONE` 
transition; `collectsAggregateLagWithoutInventingQueueMaximumTest`, 
`emptyProgressStillDoesNotProvideAQueueMaximumTest` and 
`failedGroupProgressRemainsUnavailableTest` keep `con [...]
    
    Known gap, tracked for a maintainer follow-up: 
`NativeAlertMetricCatalogService.java:41-44` still lists 
`consumer.lag.max_queue` under `BUSINESS_CLOUD`, so `validate()` (`:79-83`) 
keeps accepting cloud queue rules and the rule editor keeps offering a metric 
that can now only ever produce UNSUPPORTED — 
`docs/studio-native-alerting-design.md:147` requires the catalog to be 
capability-aware and hide unsupported metrics, and the metric table row at 
`:142` still names Aliyun and Tencent as [...]
---
 .../CloudRocketMqBusinessMetricsCollector.java     |   9 +-
 .../CloudRocketMqBusinessMetricsCollectorTest.java | 142 ++++++++++++++++++++-
 2 files changed, 140 insertions(+), 11 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
index 4cc1d6161..8503c84a3 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollector.java
@@ -88,12 +88,13 @@ public class CloudRocketMqBusinessMetricsCollector 
implements BusinessMetricsCol
         try {
             List<QueueProgressVO> progress = 
provider.getGroupProgress(instance.getName(), group.getName());
             double totalLag = progress.stream().mapToDouble(row -> 
Math.max(0L, row.getDiffTotal())).sum();
-            double maxQueueLag = progress.stream().mapToDouble(row -> 
Math.max(0L, row.getDiffTotal())).max()
-                    .orElse(0D);
             List<MetricSample> samples = new ArrayList<>();
             samples.add(available(CONSUMER_LAG_TOTAL, instance, 
group.getClusterId(), labels, totalLag, collectedAt));
-            samples.add(available(CONSUMER_LAG_MAX_QUEUE, instance, 
group.getClusterId(), labels, maxQueueLag,
-                    collectedAt));
+            // Aliyun and Tencent expose topic/group aggregates, not 
individual queues.
+            // Keep a non-numeric sample so an existing queue alert is not 
falsely cleared.
+            samples.add(new MetricSample(CONSUMER_LAG_MAX_QUEUE, 
AlertDomain.BUSINESS, instance.getName(),
+                    group.getClusterId(), labels, null, 
MetricAvailability.UNSUPPORTED, collectedAt,
+                    "CLOUD_QUEUE_LAG_UNSUPPORTED"));
             progress.stream().filter(row -> row.getTopic() != null && 
!row.getTopic().isBlank())
                     
.collect(java.util.stream.Collectors.groupingBy(QueueProgressVO::getTopic,
                             java.util.stream.Collectors.summingLong(
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollectorTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollectorTest.java
index c7ea40b4d..43dcbb536 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollectorTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/CloudRocketMqBusinessMetricsCollectorTest.java
@@ -25,8 +25,18 @@ import 
org.apache.rocketmq.studio.instance.group.QueueProgressVO;
 import org.apache.rocketmq.studio.provider.InstanceProvider;
 import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.apache.rocketmq.studio.ops.alert.AlertDomain;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleEvaluator;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
 
 import java.util.List;
+import java.time.Instant;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleState;
+import org.apache.rocketmq.studio.ops.alert.AlertStateMachine;
+import org.apache.rocketmq.studio.ops.alert.AlertStateStatus;
+import org.apache.rocketmq.studio.ops.alert.AlertStateTransition;
 import java.util.Optional;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -35,7 +45,7 @@ import static org.mockito.Mockito.when;
 
 class CloudRocketMqBusinessMetricsCollectorTest {
     @Test
-    void collectsTotalAndMaximumLagFromACloudProviderTest() {
+    void collectsAggregateLagWithoutInventingQueueMaximumTest() {
         InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
         InstanceProvider provider = mock(InstanceProvider.class);
         InstanceVO instance = 
InstanceVO.builder().name("aliyun").vendor(InstanceVendor.ALIYUN).build();
@@ -45,19 +55,137 @@ class CloudRocketMqBusinessMetricsCollectorTest {
         
when(registry.byInstanceId("aliyun")).thenReturn(Optional.of(provider));
         when(provider.listConsumerGroups("aliyun", 
null)).thenReturn(List.of(group));
         when(provider.getGroupProgress("aliyun", "orders")).thenReturn(List.of(
-                
QueueProgressVO.builder().topic("orders-topic").diffTotal(12).build(), 
QueueProgressVO.builder()
-                .topic("orders-topic").diffTotal(30).build()));
+                
QueueProgressVO.builder().topic("orders-topic").broker("topic:orders-topic")
+                        
.brokerOffset(-1).consumerOffset(-1).diffTotal(12).build(), 
QueueProgressVO.builder()
+                .topic("payments-topic").broker("topic:payments-topic")
+                        
.brokerOffset(-1).consumerOffset(-1).diffTotal(30).build()));
 
         List<MetricSample> samples = new 
CloudRocketMqBusinessMetricsCollector(registry).collect(instance);
 
-        assertThat(samples).hasSize(3).allSatisfy(sample -> 
assertThat(sample.availability())
-                .isEqualTo(MetricAvailability.AVAILABLE));
+        assertThat(samples).hasSize(4);
+        assertThat(samples).filteredOn(sample -> 
!sample.metricKey().equals("consumer.lag.max_queue"))
+                .allSatisfy(sample -> 
assertThat(sample.availability()).isEqualTo(MetricAvailability.AVAILABLE));
         assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("consumer.lag.total"))
                 
.singleElement().extracting(MetricSample::value).isEqualTo(42D);
         assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("consumer.lag.max_queue"))
-                
.singleElement().extracting(MetricSample::value).isEqualTo(30D);
+                .singleElement().satisfies(sample -> {
+                    
assertThat(sample.availability()).isEqualTo(MetricAvailability.UNSUPPORTED);
+                    assertThat(sample.value()).isNull();
+                    assertThat(sample.clusterId()).isEqualTo("cloud-a");
+                    
assertThat(sample.unavailableReason()).isEqualTo("CLOUD_QUEUE_LAG_UNSUPPORTED");
+                });
         assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("topic.backlog.total"))
-                
.singleElement().extracting(MetricSample::value).isEqualTo(42D);
+                
.extracting(MetricSample::value).containsExactlyInAnyOrder(12D, 30D);
+    }
+
+    @ParameterizedTest
+    @EnumSource(value = InstanceVendor.class, names = {"ALIYUN", "TENCENT"})
+    void doesNotPublishTopicTotalsAsQueueMaximumTest(InstanceVendor vendor) {
+        QueueProgressVO aggregate = 
QueueProgressVO.builder().topic("orders-topic")
+                .broker("topic:orders-topic").queueId(0)
+                
.brokerOffset(QueueProgressVO.UNKNOWN_OFFSET).consumerOffset(QueueProgressVO.UNKNOWN_OFFSET)
+                .diffTotal(80).build();
+        List<MetricSample> samples = collectCloudProgress(vendor, 
List.of(aggregate));
+        MetricSample maximum = samples.stream().filter(sample -> 
sample.metricKey().equals("consumer.lag.max_queue"))
+                .findFirst().orElseThrow();
+        AlertRuleVO rule = AlertRuleVO.builder().domain(AlertDomain.BUSINESS)
+                
.metric("consumer.lag.max_queue").operator(">").threshold(50).build();
+
+        // Two queues of 40 each have topic lag 80, but neither queue breaches 
50.
+        assertThat(new AlertRuleEvaluator().evaluate(rule, 
maximum).conditionMet()).isFalse();
+        
assertThat(maximum.availability()).isEqualTo(MetricAvailability.UNSUPPORTED);
+        assertThat(maximum.value()).isNull();
+        assertThat(maximum.labels()).containsEntry("consumerGroup", "orders");
+        assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("consumer.lag.total"))
+                .singleElement().satisfies(sample -> {
+                    assertThat(sample.value()).isEqualTo(80D);
+                    
assertThat(sample.availability()).isEqualTo(MetricAvailability.AVAILABLE);
+                });
+        assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("topic.backlog.total"))
+                
.singleElement().extracting(MetricSample::value).isEqualTo(80D);
+    }
+
+    @Test
+    void doesNotPublishAliyunTotalFallbackAsQueueMaximumTest() {
+        QueueProgressVO aggregate = 
QueueProgressVO.builder().broker("total").queueId(0)
+                
.brokerOffset(QueueProgressVO.UNKNOWN_OFFSET).consumerOffset(QueueProgressVO.UNKNOWN_OFFSET)
+                .diffTotal(80).build();
+        List<MetricSample> samples = 
collectCloudProgress(InstanceVendor.ALIYUN, List.of(aggregate));
+
+        assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("consumer.lag.max_queue"))
+                .singleElement().satisfies(sample -> {
+                    
assertThat(sample.availability()).isEqualTo(MetricAvailability.UNSUPPORTED);
+                    assertThat(sample.value()).isNull();
+                });
+        assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("consumer.lag.total"))
+                
.singleElement().extracting(MetricSample::value).isEqualTo(80D);
+        assertThat(samples).noneMatch(sample -> 
sample.metricKey().equals("topic.backlog.total"));
+    }
+
+    @Test
+    void unsupportedQueueMaximumDoesNotClearAnActiveAlertTest() {
+        List<MetricSample> samples = 
collectCloudProgress(InstanceVendor.TENCENT, List.of(
+                
QueueProgressVO.builder().topic("orders-topic").broker("topic:orders-topic")
+                        
.brokerOffset(QueueProgressVO.UNKNOWN_OFFSET).consumerOffset(QueueProgressVO.UNKNOWN_OFFSET)
+                        .diffTotal(0).build()));
+        MetricSample maximum = samples.stream().filter(sample -> 
sample.metricKey().equals("consumer.lag.max_queue"))
+                .findFirst().orElseThrow();
+        AlertRuleVO rule = AlertRuleVO.builder().domain(AlertDomain.BUSINESS)
+                
.metric("consumer.lag.max_queue").operator(">").threshold(50).build();
+        Instant previously = maximum.collectedAt().minusSeconds(60);
+        AlertRuleState firing = new AlertRuleState(AlertStateStatus.FIRING, 1, 
80D,
+                previously, previously, previously, null);
+
+        var update = new AlertStateMachine().advance(firing,
+                new AlertRuleEvaluator().evaluate(rule, maximum), 1, 
maximum.collectedAt());
+
+        assertThat(update.transition()).isEqualTo(AlertStateTransition.NONE);
+        assertThat(update.state().status()).isEqualTo(AlertStateStatus.FIRING);
+    }
+
+    @Test
+    void emptyProgressStillDoesNotProvideAQueueMaximumTest() {
+        List<MetricSample> samples = 
collectCloudProgress(InstanceVendor.ALIYUN, List.of());
+
+        assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("consumer.lag.max_queue"))
+                .singleElement().satisfies(sample -> {
+                    
assertThat(sample.availability()).isEqualTo(MetricAvailability.UNSUPPORTED);
+                    assertThat(sample.value()).isNull();
+                });
+        assertThat(samples).filteredOn(sample -> 
sample.metricKey().equals("consumer.lag.total"))
+                .singleElement().extracting(MetricSample::value).isEqualTo(0D);
+    }
+
+    @Test
+    void failedGroupProgressRemainsUnavailableTest() {
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        InstanceProvider provider = mock(InstanceProvider.class);
+        InstanceVO instance = 
InstanceVO.builder().name("cloud").vendor(InstanceVendor.TENCENT).build();
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("orders");
+        when(registry.byInstanceId("cloud")).thenReturn(Optional.of(provider));
+        when(provider.listConsumerGroups("cloud", 
null)).thenReturn(List.of(group));
+        when(provider.getGroupProgress("cloud", "orders")).thenThrow(new 
IllegalStateException("unavailable"));
+
+        assertThat(new 
CloudRocketMqBusinessMetricsCollector(registry).collect(instance))
+                .hasSize(3).allSatisfy(sample -> {
+                    
assertThat(sample.availability()).isEqualTo(MetricAvailability.UNAVAILABLE);
+                    assertThat(sample.value()).isNull();
+                    assertThat(sample.labels()).containsEntry("consumerGroup", 
"orders");
+                });
+    }
+
+    private static List<MetricSample> collectCloudProgress(InstanceVendor 
vendor, List<QueueProgressVO> progress) {
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        InstanceProvider provider = mock(InstanceProvider.class);
+        InstanceVO instance = 
InstanceVO.builder().name("cloud").vendor(vendor).build();
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("orders");
+        group.setClusterId("cloud-a");
+        when(registry.byInstanceId("cloud")).thenReturn(Optional.of(provider));
+        when(provider.listConsumerGroups("cloud", 
null)).thenReturn(List.of(group));
+        when(provider.getGroupProgress("cloud", 
"orders")).thenReturn(progress);
+        return new 
CloudRocketMqBusinessMetricsCollector(registry).collect(instance);
     }
 
     @Test

Reply via email to