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 aa9677b9e fix(metrics): preserve delay incidents without a timestamp 
(#6187)
aa9677b9e is described below

commit aa9677b9ea2aafa592503b3c1abfdcb58f883c75
Author: zmuxuny <[email protected]>
AuthorDate: Mon Oct 12 00:17:45 2026 -0700

    fix(metrics): preserve delay incidents without a timestamp (#6187)
    
    `ApacheRocketMqBusinessMetricsCollector.java:96-98` added a 
`consumer.delay.seconds` sample only when 
`group.isConsumptionTimestampAvailable()`, so a group whose consume stats 
report a backlog but no consumption timestamp vanished from the collection 
round entirely. `NativeAlertProcessor.reconcileMissingActiveStates` reads a key 
that is absent from `presentKeys` as a disappeared resource and advances it 
with the synthetic AVAILABLE clear built at `:139`, which resolves a delay 
alert t [...]
    
    One new collector test and a two-instance parameterized processor test pin 
this end to end through the real collector and the real processor. Removing the 
`else` empties the sample in 
`reportsUnavailableDelayWhenTheConsumptionTimestampIsMissingTest` and makes 
`preservesDelayIncidentUntilARealTimestampConfirmsRecoveryTest` resolve both 
the FIRING and the ACKED state. The second half of that test flips 
`consumptionTimestampAvailable` to true with a zero delay and asserts the state 
does  [...]
---
 .../ApacheRocketMqBusinessMetricsCollector.java    |  6 +++
 ...ApacheRocketMqBusinessMetricsCollectorTest.java | 23 +++++++++
 .../studio/ops/alert/NativeAlertProcessorTest.java | 57 ++++++++++++++++++++++
 3 files changed, 86 insertions(+)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollector.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollector.java
index c0e10eaa5..3cc3bdf37 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollector.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollector.java
@@ -95,6 +95,12 @@ public class ApacheRocketMqBusinessMetricsCollector 
implements BusinessMetricsCo
                 }
                 if (group.isConsumptionTimestampAvailable()) {
                     samples.add(delaySample(instance, group, collectedAt));
+                } else {
+                    // Stats without a consumed-message timestamp leave the 
delay unknown; emit it as
+                    // unavailable instead of omitting the sample, so 
reconciliation does not see the
+                    // group's delay series vanish and resolve an active delay 
alert for it.
+                    samples.add(unavailable(CONSUMER_DELAY_SECONDS, instance, 
group.getClusterId(),
+                            Map.of("consumerGroup", group.getName()), 
collectedAt, "CONSUMER_TIMESTAMP_UNAVAILABLE"));
                 }
                 samples.addAll(queueLagSamples(provider, instance, group, 
collectedAt));
             }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollectorTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollectorTest.java
index f84483319..9ed3b4c64 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollectorTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/collectors/ApacheRocketMqBusinessMetricsCollectorTest.java
@@ -187,6 +187,29 @@ class ApacheRocketMqBusinessMetricsCollectorTest {
                 });
     }
 
+    @Test
+    void reportsUnavailableDelayWhenTheConsumptionTimestampIsMissingTest() {
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        InstanceProvider provider = mock(InstanceProvider.class);
+        ConsumerGroupVO orders = group("orders", "cluster-a", 42);
+        orders.setConsumptionTimestampAvailable(false);
+        when(registry.byInstanceId("local")).thenReturn(Optional.of(provider));
+        when(provider.listConsumerGroups("local", 
null)).thenReturn(List.of(orders));
+        when(provider.getGroupProgress("local", 
"orders")).thenReturn(List.of(QueueProgressVO.builder()
+                .topic("orders-topic").diffTotal(17).build()));
+
+        List<MetricSample> samples = new 
ApacheRocketMqBusinessMetricsCollector(registry).collect(apacheInstance());
+
+        assertThat(samples).filteredOn(sample -> sample.metricKey().equals(
+                ApacheRocketMqBusinessMetricsCollector.CONSUMER_DELAY_SECONDS))
+                .singleElement().satisfies(sample -> {
+                    
assertThat(sample.availability()).isEqualTo(MetricAvailability.UNAVAILABLE);
+                    assertThat(sample.value()).isNull();
+                    assertThat(sample.clusterId()).isEqualTo("cluster-a");
+                    
assertThat(sample.unavailableReason()).isEqualTo("CONSUMER_TIMESTAMP_UNAVAILABLE");
+                });
+    }
+
     private static ConsumerGroupVO group(String name, String clusterId, long 
lag) {
         ConsumerGroupVO group = new ConsumerGroupVO();
         group.setName(name);
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
index 86860162c..ce0e77e09 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
@@ -24,9 +24,12 @@ import 
org.apache.rocketmq.studio.cluster.metrics.collectors.ApacheRocketMqBusin
 import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.instance.InstanceVO;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+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.springframework.transaction.PlatformTransactionManager;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.TransactionStatus;
@@ -983,6 +986,60 @@ class NativeAlertProcessorTest {
         verify(outbox, never()).enqueue(any(), any(), any());
     }
 
+    @ParameterizedTest
+    @EnumSource(value = AlertStateStatus.class, names = {"FIRING", "ACKED"})
+    void 
preservesDelayIncidentUntilARealTimestampConfirmsRecoveryTest(AlertStateStatus 
status) {
+        AlertRuleVO rule = rule("local", "orders", 1);
+        rule.setMetric("consumer.delay.seconds");
+        AlertService service = mock(AlertService.class);
+        
when(service.listRules(AlertDomain.BUSINESS)).thenReturn(List.of(rule));
+        Map<String, String> labels = Map.of("consumerGroup", "orders");
+        AlertStateKey key = new AlertStateKey(rule.getId(), 
AlertFingerprint.of(rule.getId(), "local", labels));
+        Instant firedAt = Instant.now().minusSeconds(60);
+        AtomicReference<AlertRuleState> saved = new AtomicReference<>(new 
AlertRuleState(status, 1, 120D,
+                firedAt, firedAt, firedAt, null));
+        AlertStateRepository states = mock(AlertStateRepository.class);
+        when(states.find(eq(key))).thenAnswer(invocation -> 
Optional.of(saved.get()));
+        when(states.save(eq(key), 
any(AlertRuleState.class))).thenAnswer(invocation -> {
+            saved.set(invocation.getArgument(1));
+            return true;
+        });
+        when(states.findActive(any(MetricCollectionScope.class), 
any())).thenAnswer(invocation ->
+                saved.get().status() == AlertStateStatus.RESOLVED ? List.of()
+                        : List.of(new ActiveAlertState(key, saved.get(), 
"local", labels)));
+        AlertRepository alerts = mock(AlertRepository.class);
+        NativeAlertProcessor processor = processor(service, states, alerts);
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        InstanceProvider provider = mock(InstanceProvider.class);
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("orders");
+        group.setConsumeStatsAvailable(true);
+        group.setTotalLag(100);
+        // A known backlog does not imply that the broker supplied a 
consumption timestamp.
+        group.setConsumptionTimestampAvailable(false);
+        when(registry.byInstanceId("local")).thenReturn(Optional.of(provider));
+        when(provider.listConsumerGroups("local", 
null)).thenReturn(List.of(group));
+        when(provider.getGroupProgress("local", 
"orders")).thenReturn(List.of(QueueProgressVO.builder()
+                .topic("orders-topic").diffTotal(100).build()));
+        ApacheRocketMqBusinessMetricsCollector collector = new 
ApacheRocketMqBusinessMetricsCollector(registry);
+        InstanceVO instance = 
InstanceVO.builder().name("local").vendor(InstanceVendor.APACHE).build();
+        MetricCollectionScope scope = new 
MetricCollectionScope(AlertDomain.BUSINESS, "local", collector.metricKeys());
+
+        processor.processSuccessfulCollection(scope, 
collector.collect(instance));
+
+        assertThat(saved.get().status()).isEqualTo(status);
+        assertThat(saved.get().currentValue()).isEqualTo(120D);
+        verify(alerts, never()).saveAlert(any(SystemAlertVO.class));
+
+        group.setConsumptionTimestampAvailable(true);
+        group.setDelaySeconds(0);
+        processor.processSuccessfulCollection(scope, 
collector.collect(instance));
+
+        assertThat(saved.get().status()).isEqualTo(AlertStateStatus.RESOLVED);
+        verify(alerts).saveAlert(org.mockito.ArgumentMatchers.argThat(event ->
+                "RESOLVED".equals(event.getTransition())));
+    }
+
     private static PlatformTransactionManager mockTxManager() {
         PlatformTransactionManager txManager = 
mock(PlatformTransactionManager.class);
         TransactionStatus status = mock(TransactionStatus.class);

Reply via email to