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 10b43e1f0 fix(alerts): omit reminder timestamps for non-reminding 
states (#5907)
10b43e1f0 is described below

commit 10b43e1f0150b9747209116b7fe31db7a2effc53
Author: 健康宝 <[email protected]>
AuthorDate: Mon Oct 12 15:19:50 2026 +0800

    fix(alerts): omit reminder timestamps for non-reminding states (#5907)
    
    `MybatisPlusAlertStateRepository.toRuntime` projected `nextReminderAt` for 
any state that had ever notified (`:135-136`), while `AlertStateMachine` only 
emits REMINDER while the state is FIRING with a non-zero interval — the ACKED 
branch returns `NONE` outright (`AlertStateMachine.java:67-70`) and a RESOLVED 
state's `lastNotifiedAt` is purely historical. A rule with reminders switched 
off was worst off: `AlertRuleDuration.parse` returns `Duration.ZERO` for a 
blank or `0s` interval (`: [...]
    
    `runtimeReminderIsProjectedOnlyForRemindingStatesTest` asserts all four 
rows in one `containsExactly`: ACKED, RESOLVED and FIRING-with-`0s` must be 
null, FIRING-with-`30m` must be `lastNotifiedAt + 30m`. Reverting the 
projection turns the three nulls back into timestamps; the fourth row is the 
anchor that fails if the projection is instead widened to always return null.
---
 .../ops/alert/MybatisPlusAlertStateRepository.java | 14 +++++++++--
 .../alert/MybatisPlusAlertStateRepositoryTest.java | 29 ++++++++++++++++++++++
 2 files changed, 41 insertions(+), 2 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepository.java
index 3d07e690d..bc5a99181 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepository.java
@@ -29,6 +29,7 @@ import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Repository;
 import org.springframework.util.StringUtils;
 
+import java.time.Duration;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
@@ -132,8 +133,17 @@ public class MybatisPlusAlertStateRepository implements 
AlertStateRepository {
     }
 
     private static AlertRuleRuntimeVO toRuntime(RmqAlertState entity, 
AlertRuleVO rule) {
-        LocalDateTime next = entity.getLastNotifiedAt() == null || rule == 
null ? null
-                : 
entity.getLastNotifiedAt().plus(AlertRuleDuration.parse(rule.getReminderInterval()));
+        // The state machine only emits REMINDER while FIRING with a positive 
interval; ACKED and
+        // RESOLVED states keep lastNotifiedAt without ever reminding again. 
Report nextReminderAt
+        // only for those states, instead of fabricating one for every state 
that ever notified.
+        LocalDateTime next = null;
+        if (rule != null && entity.getLastNotifiedAt() != null
+                && AlertStateStatus.FIRING.name().equals(entity.getStatus())) {
+            Duration interval = 
AlertRuleDuration.parse(rule.getReminderInterval());
+            if (!interval.isZero() && !interval.isNegative()) {
+                next = entity.getLastNotifiedAt().plus(interval);
+            }
+        }
         return 
AlertRuleRuntimeVO.builder().ruleId(entity.getRuleId()).fingerprint(entity.getFingerprint())
                 
.status(AlertStateStatus.valueOf(entity.getStatus())).consecutiveHits(entity.getConsecutiveHits()
 == null ? 0 : entity.getConsecutiveHits())
                 
.currentValue(entity.getCurrentValue()).lastNotifiedAt(entity.getLastNotifiedAt()).nextReminderAt(next).build();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepositoryTest.java
index 086a40b9b..a13f635f7 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepositoryTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/MybatisPlusAlertStateRepositoryTest.java
@@ -182,6 +182,35 @@ class MybatisPlusAlertStateRepositoryTest {
         verify(alertMapper, never()).selectOne(any());
     }
 
+    @Test
+    void runtimeReminderIsProjectedOnlyForRemindingStatesTest() {
+        RmqAlertStateMapper mapper = mock(RmqAlertStateMapper.class);
+        LocalDateTime notifiedAt = LocalDateTime.of(2026, 10, 1, 12, 0);
+        RmqAlertState acked = activeState(1L, "f1", AlertStateStatus.ACKED);
+        acked.setLastNotifiedAt(notifiedAt);
+        RmqAlertState resolved = activeState(1L, "f2", 
AlertStateStatus.RESOLVED);
+        resolved.setLastNotifiedAt(notifiedAt);
+        RmqAlertState firingWithoutReminder = activeState(2L, "f3", 
AlertStateStatus.FIRING);
+        firingWithoutReminder.setLastNotifiedAt(notifiedAt);
+        RmqAlertState firing = activeState(1L, "f4", AlertStateStatus.FIRING);
+        firing.setLastNotifiedAt(notifiedAt);
+        when(mapper.selectList(any()))
+                .thenReturn(List.of(acked, resolved, firingWithoutReminder, 
firing));
+        MybatisPlusAlertStateRepository repository = new 
MybatisPlusAlertStateRepository(mapper,
+                mock(RmqSystemAlertMapper.class));
+
+        List<AlertRuleRuntimeVO> runtimes = 
repository.findRuntimeByRuleIds(List.of(
+                AlertRuleVO.builder().id(1L).reminderInterval("30m").build(),
+                AlertRuleVO.builder().id(2L).reminderInterval("0s").build()));
+
+        assertThat(runtimes).extracting(AlertRuleRuntimeVO::getFingerprint, 
AlertRuleRuntimeVO::getNextReminderAt)
+                .containsExactly(
+                        org.assertj.core.groups.Tuple.tuple("f1", null),
+                        org.assertj.core.groups.Tuple.tuple("f2", null),
+                        org.assertj.core.groups.Tuple.tuple("f3", null),
+                        org.assertj.core.groups.Tuple.tuple("f4", 
notifiedAt.plusMinutes(30)));
+    }
+
     private static RmqAlertState activeState(Long ruleId, String fingerprint, 
AlertStateStatus status) {
         RmqAlertState state = new RmqAlertState();
         state.setRuleId(ruleId);

Reply via email to