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 038ed5dea8 [fix] jitter first cyclic schedule to avoid restart 
thundering herd (#4328)
038ed5dea8 is described below

commit 038ed5dea82d78def331e010ce51f25fd8b203c6
Author: NekoPunch <[email protected]>
AuthorDate: Mon Aug 17 21:55:02 2026 -0700

    [fix] jitter first cyclic schedule to avoid restart thundering herd (#4328)
    
    Co-authored-by: aias00 <[email protected]>
---
 .../hertzbeat/collector/timer/TimerDispatcher.java | 18 +++++++++-
 .../collector/timer/TimerDispatcherTest.java       | 39 ++++++++++++++++++++++
 2 files changed, 56 insertions(+), 1 deletion(-)

diff --git 
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
 
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
index 797da5254f..ecca28714d 100644
--- 
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
+++ 
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Supplier;
@@ -116,7 +117,7 @@ public class TimerDispatcher implements TimerDispatch, 
DisposableBean {
         // Delay dispatcher lookup to avoid a startup cycle with 
CommonDispatcher.
         WheelTimerTask timerJob = new WheelTimerTask(addJob, 
metricsTaskDispatchSupplier);
         if (addJob.isCyclic()) {
-            Long nextExecutionTime = getNextExecutionInterval(addJob);
+            long nextExecutionTime = initialCyclicDelay(addJob);
             Timeout timeout = wheelTimer.newTimeout(timerJob, 
nextExecutionTime, TimeUnit.SECONDS);
             cancelPreviousTimeout(currentCyclicTaskMap.put(addJob.getId(), 
timeout));
         } else {
@@ -205,6 +206,21 @@ public class TimerDispatcher implements TimerDispatch, 
DisposableBean {
         }
     }
 
+    /**
+     * Interval jobs get a random first-run phase: a restart re-adds every job 
at once,
+     * and a shared phase makes them all collect at the same instant forever. 
Cron jobs
+     * keep their meaningful phase; already-executed or re-added jobs keep 
theirs.
+     */
+    long initialCyclicDelay(Job addJob) {
+        long nextExecutionTime = getNextExecutionInterval(addJob);
+        boolean fixedPhase = 
ScheduleTypeEnum.CRON.getType().equals(addJob.getScheduleType());
+        if (!fixedPhase && addJob.getDispatchTime() <= 0
+                && !currentCyclicTaskMap.containsKey(addJob.getId()) && 
nextExecutionTime > 1) {
+            nextExecutionTime = 
ThreadLocalRandom.current().nextLong(nextExecutionTime) + 1;
+        }
+        return nextExecutionTime;
+    }
+
     public Long getNextExecutionInterval(Job job) {
         if (ScheduleTypeEnum.CRON.getType().equals(job.getScheduleType()) && 
job.getCronExpression() != null && !job.getCronExpression().isEmpty()) {
             try {
diff --git 
a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java
 
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java
index 17cd06a345..38fabf72bb 100644
--- 
a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java
+++ 
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java
@@ -18,8 +18,10 @@
 package org.apache.hertzbeat.collector.timer;
 
 import java.lang.reflect.Field;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum;
 import org.apache.hertzbeat.collector.dispatch.MetricsTaskDispatch;
@@ -299,4 +301,41 @@ public class TimerDispatcherTest {
             throw new AssertionError(e);
         }
     }
+
+    @Test
+    void testFirstCyclicDelayJittersWithinInterval() {
+        when(job.isCyclic()).thenReturn(true);
+        when(job.getScheduleType()).thenReturn(null);
+        when(job.getDispatchTime()).thenReturn(0L);
+        when(job.getInterval()).thenReturn(600L);
+        when(job.getId()).thenReturn(4242L);
+
+        Set<Long> delays = new HashSet<>();
+        for (int i = 0; i < 50; i++) {
+            long delay = timerDispatcher.initialCyclicDelay(job);
+            assertTrue(delay >= 1 && delay <= 600, "delay out of interval: " + 
delay);
+            delays.add(delay);
+        }
+        assertTrue(delays.size() > 1, "no jitter observed across 50 samples");
+    }
+
+    @Test
+    void testExecutedJobKeepsRemainingIntervalOnReAdd() {
+        when(job.getScheduleType()).thenReturn(null);
+        when(job.getDispatchTime()).thenReturn(System.currentTimeMillis() - 
10_000L);
+        when(job.getInterval()).thenReturn(600L);
+
+        long expected = timerDispatcher.getNextExecutionInterval(job);
+        long actual = timerDispatcher.initialCyclicDelay(job);
+        assertTrue(Math.abs(actual - expected) <= 1, "remaining interval must 
not be jittered");
+    }
+
+    @Test
+    void testCronScheduleKeepsFixedPhase() {
+        
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
+        when(job.getCronExpression()).thenReturn("0 0 3 * * ?");
+        long expected = timerDispatcher.getNextExecutionInterval(job);
+        long actual = timerDispatcher.initialCyclicDelay(job);
+        assertTrue(Math.abs(actual - expected) <= 1, "cron phase must not be 
jittered");
+    }
 }


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

Reply via email to