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 df597c099 fix(alert): preserve silence end boundaries across DST gaps
(#6422)
df597c099 is described below
commit df597c0998b5a084463ed4ff2537c144691149f2
Author: Loyal-Young <[email protected]>
AuthorDate: Thu Oct 15 18:10:09 2026 +0800
fix(alert): preserve silence end boundaries across DST gaps (#6422)
---
.../studio/ops/alert/AlertSilenceSchedule.java | 18 ++++------
.../studio/ops/alert/AlertSilenceScheduleTest.java | 42 ++++++++++++++++++++++
2 files changed, 48 insertions(+), 12 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 46390d1c0..3aa931172 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
@@ -20,7 +20,6 @@ import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@@ -62,8 +61,12 @@ final class AlertSilenceSchedule {
if (!runsOn(recurrence, silence.getRecurrenceDays(),
candidateDate)) {
continue;
}
- ZonedDateTime candidateStart = resolve(zone, candidateDate,
seedStart.toLocalTime(), preferredStartOffset);
- ZonedDateTime candidateEnd = resolveEnd(zone, candidateStart,
wallDuration, preferredEndOffset);
+ LocalDateTime scheduledStart = LocalDateTime.of(candidateDate,
seedStart.toLocalTime());
+ ZonedDateTime candidateStart =
ZonedDateTime.ofLocal(scheduledStart, zone, preferredStartOffset);
+ // Resolve each wall-clock boundary independently: adjusting a
start inside a DST gap
+ // must not shift an otherwise valid end by the gap a second time.
+ ZonedDateTime candidateEnd =
ZonedDateTime.ofLocal(scheduledStart.plus(wallDuration),
+ zone, preferredEndOffset);
Instant start = candidateStart.toInstant();
Instant end = candidateEnd.toInstant();
Instant scheduleStart =
silence.getStartsAt().toInstant(ZoneOffset.UTC);
@@ -87,15 +90,6 @@ final class AlertSilenceSchedule {
|| recurrenceDays != null &&
recurrenceDays.contains(candidateDate.getDayOfWeek().getValue());
}
- 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,
- ZoneOffset preferredOffset) {
- return
ZonedDateTime.ofLocal(start.toLocalDateTime().plus(wallDuration), zone,
preferredOffset);
- }
-
private static boolean isInside(LocalDateTime now, LocalDateTime start,
LocalDateTime end) {
return !now.isBefore(start) && now.isBefore(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 d3c8817f6..78331e68a 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
@@ -147,6 +147,48 @@ class AlertSilenceScheduleTest {
assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2027-11-07T05:45:00"))).isNull();
}
+ @Test
+ void gapAdjustedStartDoesNotMoveTheConfiguredEndTest() {
+ AlertSilenceVO silence = recurring(AlertSilenceRecurrence.DAILY,
"America/New_York", Set.of(),
+ "2026-03-07T07:30:00", "2026-03-07T09:00:00",
"2026-03-12T00:00:00");
+
+ // 02:30 moves to 03:30 in the gap, but the configured 04:00 end still
exists.
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-03-08T07:45:00")))
+ .isEqualTo(at("2026-03-08T08:00:00"));
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-03-08T08:15:00"))).isNull();
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-03-09T06:45:00")))
+ .isEqualTo(at("2026-03-09T08:00:00"));
+ }
+
+ @Test
+ void weeklyWindowDoesNotExtendWhenItsStartFallsInAGapTest() {
+ AlertSilenceVO silence = recurring(AlertSilenceRecurrence.WEEKLY,
"America/New_York", Set.of(7),
+ "2026-03-01T07:30:00", "2026-03-01T09:00:00",
"2026-03-12T00:00:00");
+
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-03-08T07:45:00")))
+ .isEqualTo(at("2026-03-08T08:00:00"));
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-03-08T08:00:00"))).isNull();
+ }
+
+ @Test
+ void halfHourGapDoesNotMoveTheConfiguredEndTest() {
+ AlertSilenceVO silence = recurring(AlertSilenceRecurrence.DAILY,
"Australia/Lord_Howe", Set.of(),
+ "2026-10-02T15:45:00", "2026-10-02T16:30:00",
"2026-10-06T00:00:00");
+
+ // The October 4 gap moves 02:15 to 02:45; the end remains 03:00.
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-10-03T15:50:00")))
+ .isEqualTo(at("2026-10-03T16:00:00"));
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-10-03T16:15:00"))).isNull();
+ }
+
+ @Test
+ void gapThatMovesStartPastEndDoesNotCreateAnExtraWindowTest() {
+ AlertSilenceVO silence = recurring(AlertSilenceRecurrence.DAILY,
"America/New_York", Set.of(),
+ "2026-03-07T07:30:00", "2026-03-07T08:15:00",
"2026-03-12T00:00:00");
+
+ assertThat(AlertSilenceSchedule.activeUntil(silence,
at("2026-03-08T07: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();