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 5cb7a8182 fix(alerts): honor the seed offset of recurring silences 
(#5896)
5cb7a8182 is described below

commit 5cb7a818206bb5937fc4f1c8e7ff3f95cec0b311
Author: 健康宝 <[email protected]>
AuthorDate: Mon Oct 12 15:19:42 2026 +0800

    fix(alerts): honor the seed offset of recurring silences (#5896)
    
    Recurring silences rebuilt each occurrence with 
`ZonedDateTime.of(LocalDateTime, ZoneId)` (`AlertSilenceSchedule.java:84-90`), 
which always resolves an ambiguous wall-clock time to the earlier offset. 
`AlertSilenceService.java:157-158` stores the seed as a UTC `LocalDateTime`, so 
a silence seeded on the second pass of a fall-back day — 01:30 in 
`America/New_York` on 2026-11-01 exists at both 05:30Z and 06:30Z — was rebuilt 
at 05:30Z, and the `start.isBefore(scheduleStart)` guard at `: [...]
    
    `recurrenceHonorsSeedOffsetDuringFallBackOverlapTest` and 
`recurrenceKeepsSeedOffsetOnLaterOverlapDaysTest` each assert a hit and a miss 
around the boundary rather than one side only. Reverting to `ZonedDateTime.of` 
makes the first return null where the end of the second-pass window is 
expected, and makes the second match a window it should not; the pre-existing 
`dailyWindowKeepsWallClockTimesAcrossSpringDstTest` is the control against 
over-correcting the spring-forward case.
---
 .../studio/ops/alert/AlertSilenceSchedule.java     | 19 +++++++++++-----
 .../studio/ops/alert/AlertSilenceScheduleTest.java | 26 ++++++++++++++++++++++
 2 files changed, 39 insertions(+), 6 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceSchedule.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceSchedule.java
index 13f593e75..46390d1c0 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceSchedule.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceSchedule.java
@@ -46,6 +46,12 @@ final class AlertSilenceSchedule {
         ZonedDateTime localNow = now.atZone(zone);
         ZonedDateTime seedStart = 
silence.getStartsAt().toInstant(ZoneOffset.UTC).atZone(zone);
         ZonedDateTime seedEnd = 
silence.getEndsAt().toInstant(ZoneOffset.UTC).atZone(zone);
+        // Rebuild occurrences with the offsets the seed was created with: on 
fall-back days the
+        // wall-clock start (and end) exists twice, and 
ZonedDateTime.of(LocalDateTime, ZoneId)
+        // alone always picks the earlier offset. That both skips the seeded 
window itself (the
+        // rebuilt start lands before startsAt) and drifts later occurrences 
to the earlier pass.
+        ZoneOffset preferredStartOffset = seedStart.getOffset();
+        ZoneOffset preferredEndOffset = seedEnd.getOffset();
         Duration wallDuration = Duration.between(seedStart.toLocalDateTime(), 
seedEnd.toLocalDateTime());
         // A weekly window may span up to 7 days, so an occurrence anchored 7 
days
         // back can still be in its final hours; daily windows span at most 24 
hours.
@@ -56,8 +62,8 @@ final class AlertSilenceSchedule {
             if (!runsOn(recurrence, silence.getRecurrenceDays(), 
candidateDate)) {
                 continue;
             }
-            ZonedDateTime candidateStart = resolve(zone, candidateDate, 
seedStart.toLocalTime());
-            ZonedDateTime candidateEnd = resolveEnd(zone, candidateStart, 
wallDuration);
+            ZonedDateTime candidateStart = resolve(zone, candidateDate, 
seedStart.toLocalTime(), preferredStartOffset);
+            ZonedDateTime candidateEnd = resolveEnd(zone, candidateStart, 
wallDuration, preferredEndOffset);
             Instant start = candidateStart.toInstant();
             Instant end = candidateEnd.toInstant();
             Instant scheduleStart = 
silence.getStartsAt().toInstant(ZoneOffset.UTC);
@@ -81,12 +87,13 @@ final class AlertSilenceSchedule {
                 || recurrenceDays != null && 
recurrenceDays.contains(candidateDate.getDayOfWeek().getValue());
     }
 
-    private static ZonedDateTime resolve(ZoneId zone, LocalDate date, 
LocalTime time) {
-        return ZonedDateTime.of(LocalDateTime.of(date, time), zone);
+    private static ZonedDateTime resolve(ZoneId zone, LocalDate date, 
LocalTime time, ZoneOffset preferredOffset) {
+        return ZonedDateTime.ofLocal(LocalDateTime.of(date, time), zone, 
preferredOffset);
     }
 
-    private static ZonedDateTime resolveEnd(ZoneId zone, ZonedDateTime start, 
Duration wallDuration) {
-        return ZonedDateTime.of(start.toLocalDateTime().plus(wallDuration), 
zone);
+    private static ZonedDateTime resolveEnd(ZoneId zone, ZonedDateTime start, 
Duration wallDuration,
+            ZoneOffset preferredOffset) {
+        return 
ZonedDateTime.ofLocal(start.toLocalDateTime().plus(wallDuration), zone, 
preferredOffset);
     }
 
     private static boolean isInside(LocalDateTime now, LocalDateTime start, 
LocalDateTime end) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceScheduleTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceScheduleTest.java
index efe2cc395..d3c8817f6 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceScheduleTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceScheduleTest.java
@@ -121,6 +121,32 @@ class AlertSilenceScheduleTest {
                 .isEqualTo(at("2026-03-09T07:30:00"));
     }
 
+    @Test
+    void recurrenceHonorsSeedOffsetDuringFallBackOverlapTest() {
+        // On 2026-11-01 in America/New_York the wall clock 01:30 occurs twice:
+        // 05:30Z (EDT) and 06:30Z (EST). This window was seeded at the second
+        // occurrence with EST offsets, so the first window is 06:30Z-07:30Z.
+        AlertSilenceVO silence = recurring(AlertSilenceRecurrence.DAILY, 
"America/New_York", Set.of(),
+                "2026-11-01T06:30:00", "2026-11-01T07:30:00", 
"2026-11-10T00:00:00");
+
+        assertThat(AlertSilenceSchedule.activeUntil(silence, 
at("2026-11-01T06:45:00")))
+                .isEqualTo(at("2026-11-01T07:30:00"));
+        assertThat(AlertSilenceSchedule.activeUntil(silence, 
at("2026-11-01T05:45:00"))).isNull();
+    }
+
+    @Test
+    void recurrenceKeepsSeedOffsetOnLaterOverlapDaysTest() {
+        // Seeded with EST offsets (06:30Z start). A year later, on the next 
overlap
+        // day (2027-11-07), the occurrence must keep the seeded 06:30Z start 
instead
+        // of drifting to the earlier 05:30Z pass.
+        AlertSilenceVO silence = recurring(AlertSilenceRecurrence.DAILY, 
"America/New_York", Set.of(),
+                "2026-11-01T06:30:00", "2026-11-01T07:30:00", 
"2027-11-10T00:00:00");
+
+        assertThat(AlertSilenceSchedule.activeUntil(silence, 
at("2027-11-07T06:45:00")))
+                .isEqualTo(at("2027-11-07T07:30:00"));
+        assertThat(AlertSilenceSchedule.activeUntil(silence, 
at("2027-11-07T05:45:00"))).isNull();
+    }
+
     private static AlertSilenceVO once(String start, String end) {
         return AlertSilenceVO.builder().startsAt(at(start)).endsAt(at(end))
                 
.recurrence(AlertSilenceRecurrence.ONCE).recurrenceDays(Set.of()).build();

Reply via email to