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();