This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/master by this push:
     new cc8206062f [fix] Fixed matching issues caused by date components and 
outdated time zone offsets (#4192)
cc8206062f is described below

commit cc8206062f19e72c63e73d2f2ea7fff7e95bfd0c
Author: Duansg <[email protected]>
AuthorDate: Mon Jul 13 22:23:02 2026 +0800

    [fix] Fixed matching issues caused by date components and outdated time 
zone offsets (#4192)
    
    Co-authored-by: Claude Fable 5 <[email protected]>
---
 .../hertzbeat/alert/reduce/AlarmSilenceReduce.java |  29 +++--
 .../service/impl/NoticeConfigServiceImpl.java      | 137 +++++++++++----------
 .../alert/service/NoticeConfigServiceTest.java     |  45 +++++++
 .../hertzbeat/common/util/BackoffUtilsTest.java    |  10 +-
 4 files changed, 145 insertions(+), 76 deletions(-)

diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java
index 7dfad6360d..77aaf0011f 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java
@@ -19,9 +19,11 @@ package org.apache.hertzbeat.alert.reduce;
 
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.ZoneId;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+
 import lombok.RequiredArgsConstructor;
 import org.apache.hertzbeat.alert.dao.AlertSilenceDao;
 import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch;
@@ -44,6 +46,7 @@ public class AlarmSilenceReduce {
     /**
      * Process alert with silence rules
      * If alert matches any active silence rule, it will be silenced
+     *
      * @param groupAlert The alert to be processed
      */
     public void silenceAlarm(GroupAlert groupAlert) {
@@ -52,7 +55,7 @@ public class AlarmSilenceReduce {
             alertSilenceList = alertSilenceDao.findAlertSilencesByEnableTrue();
             CacheFactory.setAlertSilenceCache(alertSilenceList);
         }
-        
+
         // Check each silence rule
         for (AlertSilence alertSilence : alertSilenceList) {
             // Check if alert matches silence rule
@@ -60,10 +63,10 @@ public class AlarmSilenceReduce {
             if (!match && groupAlert.getGroupLabels() != null) {
                 Map<String, String> labels = alertSilence.getLabels();
                 Map<String, String> alertLabels = groupAlert.getGroupLabels();
-                match = labels.entrySet().stream().anyMatch(item -> 
+                match = labels.entrySet().stream().anyMatch(item ->
                     alertLabels.containsKey(item.getKey()) && 
item.getValue().equals(alertLabels.get(item.getKey())));
             }
-            
+
             if (match) {
                 LocalDateTime now = LocalDateTime.now();
                 if (alertSilence.getType() == 0) {
@@ -76,22 +79,23 @@ public class AlarmSilenceReduce {
                 } else if (alertSilence.getType() == 1) {
                     // Cyclic silence rule
                     int currentDayOfWeek = now.getDayOfWeek().getValue();
-                    if (alertSilence.getDays() != null && 
alertSilence.getDays().contains((byte) currentDayOfWeek) 
-                            && !checkAndSave(now, alertSilence)) {
+                    if (alertSilence.getDays() != null && 
alertSilence.getDays().contains((byte) currentDayOfWeek)
+                        && !checkAndSave(now, alertSilence)) {
                         // Alert is silenced
                         return;
                     }
                 }
             }
         }
-        
+
         // No matching silence rule, forward the alert
         dispatcherAlarm.dispatchAlarm(groupAlert);
     }
 
     /**
      * Check if alert time is within silence period and update silence rule 
counter
-     * @param now Current time
+     *
+     * @param now          Current time
      * @param alertSilence Silence rule to check
      * @return true if alert should not be silenced, false if alert should be 
silenced
      */
@@ -100,8 +104,11 @@ public class AlarmSilenceReduce {
         boolean endMatch;
         if (alertSilence.getType() == 1) {
             LocalTime nowTime = now.toLocalTime();
-            LocalTime startTime = alertSilence.getPeriodStart() == null ? null 
: alertSilence.getPeriodStart().toLocalTime();
-            LocalTime endTime = alertSilence.getPeriodEnd() == null ? null : 
alertSilence.getPeriodEnd().toLocalTime();
+            // compare wall-clock times in the server time zone, the stored 
offset may differ from it
+            LocalTime startTime = alertSilence.getPeriodStart() == null
+                ? null : 
alertSilence.getPeriodStart().withZoneSameInstant(ZoneId.systemDefault()).toLocalTime();
+            LocalTime endTime = alertSilence.getPeriodEnd() == null
+                ? null : 
alertSilence.getPeriodEnd().withZoneSameInstant(ZoneId.systemDefault()).toLocalTime();
             if (startTime == null && endTime == null) {
                 startMatch = true;
                 endMatch = true;
@@ -121,9 +128,9 @@ public class AlarmSilenceReduce {
             }
         } else {
             startMatch = alertSilence.getPeriodStart() == null
-                    || 
now.isAfter(alertSilence.getPeriodStart().toLocalDateTime());
+                || 
now.isAfter(alertSilence.getPeriodStart().withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime());
             endMatch = alertSilence.getPeriodEnd() == null
-                    || 
now.isBefore(alertSilence.getPeriodEnd().toLocalDateTime());
+                || 
now.isBefore(alertSilence.getPeriodEnd().withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime());
         }
 
         if (startMatch && endMatch) {
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
index dc4521aabb..24a719b5bf 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/NoticeConfigServiceImpl.java
@@ -52,6 +52,7 @@ import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.ZoneId;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -91,7 +92,7 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
             Predicate predicate = criteriaBuilder.conjunction();
             if (StringUtils.isNotBlank(name)) {
                 Predicate predicateName = criteriaBuilder.like(
-                        criteriaBuilder.lower(root.get("name")), "%" + 
name.toLowerCase() + "%"
+                    criteriaBuilder.lower(root.get("name")), "%" + 
name.toLowerCase() + "%"
                 );
                 predicate = criteriaBuilder.and(predicateName);
             }
@@ -113,9 +114,9 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
 
             // Filter by name (case-insensitive)
             List<NoticeTemplate> filteredDefaultTemplates = 
defaultTemplates.stream()
-                    .filter(template -> StringUtils.isBlank(name)
-                            || 
template.getName().toLowerCase().contains(name.toLowerCase()))
-                    .collect(Collectors.toList());
+                .filter(template -> StringUtils.isBlank(name)
+                    || 
template.getName().toLowerCase().contains(name.toLowerCase()))
+                .collect(Collectors.toList());
 
             // Pagination logic
             int totalItems = filteredDefaultTemplates.size();
@@ -134,7 +135,7 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
                 Predicate predicate = criteriaBuilder.conjunction();
                 if (StringUtils.isNotBlank(name)) {
                     Predicate predicateName = criteriaBuilder.like(
-                            criteriaBuilder.lower(root.get("name")), "%" + 
name.toLowerCase() + "%"
+                        criteriaBuilder.lower(root.get("name")), "%" + 
name.toLowerCase() + "%"
                     );
                     predicate = criteriaBuilder.and(predicateName);
                 }
@@ -146,7 +147,6 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
     }
 
 
-
     @Override
     public List<NoticeTemplate> getAllNoticeTemplates() {
         List<NoticeTemplate> defaultTemplates = new 
LinkedList<>(PRESET_TEMPLATE.values());
@@ -160,7 +160,7 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
             Predicate predicate = criteriaBuilder.conjunction();
             if (StringUtils.isNotBlank(name)) {
                 Predicate predicateName = criteriaBuilder.like(
-                        criteriaBuilder.lower(root.get("name")), "%" + 
name.toLowerCase() + "%"
+                    criteriaBuilder.lower(root.get("name")), "%" + 
name.toLowerCase() + "%"
                 );
                 predicate = criteriaBuilder.and(predicateName);
             }
@@ -217,45 +217,56 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
         //  one alert still notifies the whole group. The ideal design is 
route-then-group (like Alertmanager):
         //  route each single alert by its labels first, then group per 
receiver. Tracked as a follow-up to #3852.
         return rules.stream()
-                .filter(rule -> {
-                    if (!rule.isFilterAll()) {
-                        // filter labels: a rule matches when ANY single alert 
in the group carries
-                        if (rule.getLabels() != null && 
!rule.getLabels().isEmpty()) {
-                            List<SingleAlert> singleAlerts = alert.getAlerts();
-                            boolean labelMatch = singleAlerts != null && 
singleAlerts.stream().anyMatch(singleAlert -> {
-                                Map<String, String> alertLabels = 
singleAlert.getLabels();
-                                if (alertLabels == null) {
-                                    return false;
-                                }
-                                return 
rule.getLabels().entrySet().stream().allMatch(labelItem ->
-                                        Objects.equals(labelItem.getValue(), 
alertLabels.get(labelItem.getKey())));
-                            });
-                            if (!labelMatch) {
+            .filter(rule -> {
+                if (!rule.isFilterAll()) {
+                    // filter labels: a rule matches when ANY single alert in 
the group carries
+                    if (rule.getLabels() != null && 
!rule.getLabels().isEmpty()) {
+                        List<SingleAlert> singleAlerts = alert.getAlerts();
+                        boolean labelMatch = singleAlerts != null && 
singleAlerts.stream().anyMatch(singleAlert -> {
+                            Map<String, String> alertLabels = 
singleAlert.getLabels();
+                            if (alertLabels == null) {
                                 return false;
                             }
+                            return 
rule.getLabels().entrySet().stream().allMatch(labelItem ->
+                             Objects.equals(labelItem.getValue(), 
alertLabels.get(labelItem.getKey())));
+                        });
+                        if (!labelMatch) {
+                            return false;
                         }
                     }
+                }
 
-                    LocalDateTime nowDate = LocalDateTime.now();
-                    // filter day
-                    int currentDayOfWeek = 
nowDate.toLocalDate().getDayOfWeek().getValue();
-                    if (rule.getDays() != null && !rule.getDays().isEmpty()) {
-                        boolean dayMatch = 
rule.getDays().stream().anyMatch(item -> item == currentDayOfWeek);
-                        if (!dayMatch) {
-                            return false;
-                        }
+                LocalDateTime nowDate = LocalDateTime.now();
+                // filter day
+                int currentDayOfWeek = 
nowDate.toLocalDate().getDayOfWeek().getValue();
+                if (rule.getDays() != null && !rule.getDays().isEmpty()) {
+                    boolean dayMatch = rule.getDays().stream().anyMatch(item 
-> item == currentDayOfWeek);
+                    if (!dayMatch) {
+                        return false;
                     }
-                    // filter time
-                    LocalTime nowTime = nowDate.toLocalTime();
-                    boolean startMatch = rule.getPeriodStart() == null
-                            || 
nowTime.isAfter(rule.getPeriodStart().toLocalTime())
-                            || (rule.getPeriodEnd() != null && 
rule.getPeriodStart().isAfter(rule.getPeriodEnd())
-                                    && 
nowTime.isBefore(rule.getPeriodStart().toLocalTime()));
-                    boolean endMatch = rule.getPeriodEnd() == null
-                            || 
nowTime.isBefore(rule.getPeriodEnd().toLocalTime());
-                    return startMatch && endMatch;
-                })
-                .collect(Collectors.toList());
+                }
+                // filter time, compare wall-clock times in the server time 
zone,
+                // the stored date part is meaningless (it is the day the user 
picked the time on the ui)
+                LocalTime nowTime = nowDate.toLocalTime();
+                LocalTime startTime = rule.getPeriodStart() == null
+                    ? null : 
rule.getPeriodStart().withZoneSameInstant(ZoneId.systemDefault()).toLocalTime();
+                LocalTime endTime = rule.getPeriodEnd() == null
+                    ? null : 
rule.getPeriodEnd().withZoneSameInstant(ZoneId.systemDefault()).toLocalTime();
+                if (startTime == null && endTime == null) {
+                    return true;
+                }
+                if (startTime == null) {
+                    return !nowTime.isAfter(endTime);
+                }
+                if (endTime == null) {
+                    return !nowTime.isBefore(startTime);
+                }
+                if (!startTime.isAfter(endTime)) {
+                    return !nowTime.isBefore(startTime) && 
!nowTime.isAfter(endTime);
+                }
+                // cross-midnight window, e.g. 22:00-06:00
+                return !nowTime.isBefore(startTime) || 
!nowTime.isAfter(endTime);
+            }).collect(Collectors.toList());
     }
 
     @Override
@@ -314,31 +325,31 @@ public class NoticeConfigServiceImpl implements 
NoticeConfigService, CommandLine
         Map<String, String> annotations = new HashMap<>(8);
         annotations.put("suggest", "Please check the CPU usage of the server");
         SingleAlert singleAlert1 = SingleAlert.builder()
-                .labels(labels)
-                .content("test send msg! \\n This is the test data. It is 
proved that it can be received successfully")
-                .startAt(System.currentTimeMillis())
-                .activeAt(System.currentTimeMillis())
-                .endAt(System.currentTimeMillis())
-                .triggerTimes(2)
-                .annotations(annotations)
-                .status("firing")
-                .build();
+            .labels(labels)
+            .content("test send msg! \\n This is the test data. It is proved 
that it can be received successfully")
+            .startAt(System.currentTimeMillis())
+            .activeAt(System.currentTimeMillis())
+            .endAt(System.currentTimeMillis())
+            .triggerTimes(2)
+            .annotations(annotations)
+            .status("firing")
+            .build();
         SingleAlert singleAlert2 = SingleAlert.builder()
-                .labels(labels)
-                .content("test send msg! \\n This is the test data. It is 
proved that it can be received successfully")
-                .startAt(System.currentTimeMillis())
-                .activeAt(System.currentTimeMillis())
-                .endAt(System.currentTimeMillis())
-                .triggerTimes(4)
-                .annotations(annotations)
-                .status("firing")
-                .build();
+            .labels(labels)
+            .content("test send msg! \\n This is the test data. It is proved 
that it can be received successfully")
+            .startAt(System.currentTimeMillis())
+            .activeAt(System.currentTimeMillis())
+            .endAt(System.currentTimeMillis())
+            .triggerTimes(4)
+            .annotations(annotations)
+            .status("firing")
+            .build();
         GroupAlert groupAlert = GroupAlert.builder()
-                .commonLabels(Map.of(CommonConstants.LABEL_ALERT_NAME, "CPU 
Usage Alert"))
-                .commonAnnotations(annotations)
-                .alerts(List.of(singleAlert1, singleAlert2))
-                .status("firing")
-                .build();
+            .commonLabels(Map.of(CommonConstants.LABEL_ALERT_NAME, "CPU Usage 
Alert"))
+            .commonAnnotations(annotations)
+            .alerts(List.of(singleAlert1, singleAlert2))
+            .status("firing")
+            .build();
         return dispatcherAlarm.sendNoticeMsg(noticeReceiver, null, groupAlert);
     }
 
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
index 81a9c2c24b..4206140963 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/NoticeConfigServiceTest.java
@@ -41,7 +41,10 @@ import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.domain.Specification;
 
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -379,4 +382,46 @@ class NoticeConfigServiceTest {
         assertEquals(1, matched.size());
         assertEquals(4L, matched.get(0).getId());
     }
+
+    @Test
+    void getReceiverFilterRuleMatchesPeriodContainingNow() {
+        ZonedDateTime now = ZonedDateTime.now();
+        List<NoticeRule> matched = filterWithPeriod(now.minusHours(6), 
now.plusHours(6));
+        assertEquals(1, matched.size());
+    }
+
+    @Test
+    void getReceiverFilterRuleFiltersPeriodExcludingNow() {
+        ZonedDateTime now = ZonedDateTime.now();
+        List<NoticeRule> matched = filterWithPeriod(now.plusHours(1), 
now.plusHours(2));
+        assertEquals(0, matched.size());
+    }
+
+    @Test
+    void getReceiverFilterRuleMatchesCrossMidnightPeriod() {
+        ZonedDateTime now = ZonedDateTime.now();
+        List<NoticeRule> matched = filterWithPeriod(now.minusHours(1), 
now.minusHours(2).plusDays(1));
+        assertEquals(1, matched.size());
+    }
+
+    @Test
+    void getReceiverFilterRuleNormalizesStoredOffsetToServerZone() {
+        ZonedDateTime now = ZonedDateTime.now();
+        List<NoticeRule> matched = filterWithPeriod(
+                now.minusHours(6).withZoneSameInstant(ZoneOffset.ofHours(-7)),
+                now.plusHours(6).withZoneSameInstant(ZoneOffset.ofHours(9)));
+        assertEquals(1, matched.size());
+    }
+
+    private List<NoticeRule> filterWithPeriod(ZonedDateTime periodStart, 
ZonedDateTime periodEnd) {
+        NoticeRule rule = new NoticeRule();
+        rule.setId(10L);
+        rule.setName("PeriodRule");
+        rule.setFilterAll(true);
+        rule.setPeriodStart(periodStart);
+        rule.setPeriodEnd(periodEnd);
+        CacheFactory.clearNoticeCache();
+        
when(noticeRuleDao.findNoticeRulesByEnableTrue()).thenReturn(Collections.singletonList(rule));
+        return noticeConfigService.getReceiverFilterRule(new GroupAlert());
+    }
 }
diff --git 
a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/BackoffUtilsTest.java
 
b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/BackoffUtilsTest.java
index f97bb8e5ba..9ecc68b40d 100644
--- 
a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/BackoffUtilsTest.java
+++ 
b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/BackoffUtilsTest.java
@@ -40,6 +40,7 @@ class BackoffUtilsTest {
         ExponentialBackoff backoff = new ExponentialBackoff(10L, 100L);
         boolean shouldContinue = 
BackoffUtils.shouldContinueAfterBackoff(backoff);
         assertFalse(shouldContinue);
+        assertTrue(Thread.interrupted());
     }
 
     @Test
@@ -60,9 +61,14 @@ class BackoffUtilsTest {
 
         boolean shouldContinue = 
BackoffUtils.shouldContinueAfterBackoff(backoff);
 
+        // read and clear the restored interrupt status before join(),
+        // otherwise join() throws InterruptedException when the
+        // interrupting thread is still alive at this point
+        boolean interrupted = Thread.interrupted();
+
         interruptingThread.join();
 
         assertFalse(shouldContinue);
-        assertTrue(Thread.currentThread().isInterrupted());
+        assertTrue(interrupted);
     }
-}
\ No newline at end of file
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to