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

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new ac49f5fa107 NIFI-16515 Corrected CRON scheduling to only execute once 
after Daylight Saving Time changes (#11867)
ac49f5fa107 is described below

commit ac49f5fa107ab06ddb20b5e113bd1868289a4c5e
Author: dan-s1 <[email protected]>
AuthorDate: Fri Oct 30 13:37:05 2026 -0400

    NIFI-16515 Corrected CRON scheduling to only execute once after Daylight 
Saving Time changes (#11867)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../controller/scheduling/CronSchedulingAgent.java | 31 +++++++++++++---------
 .../scheduling/CronSchedulingAgentTest.java        | 23 ++++++++++++++++
 2 files changed, 42 insertions(+), 12 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/CronSchedulingAgent.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/CronSchedulingAgent.java
index 17eae44f8c7..a78b56f0386 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/CronSchedulingAgent.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/scheduling/CronSchedulingAgent.java
@@ -25,7 +25,7 @@ import org.apache.nifi.engine.FlowEngine;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.springframework.scheduling.support.CronExpression;
 
-import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.ScheduledFuture;
@@ -67,12 +67,12 @@ public class CronSchedulingAgent extends 
AbstractTimeBasedSchedulingAgent {
 
         final ReportingTaskWrapper taskWrapper = new 
ReportingTaskWrapper(taskNode, scheduleState, 
flowController.getExtensionManager());
 
-        final OffsetDateTime initialDate = getInitialDate(cronExpression);
+        final ZonedDateTime initialDate = getInitialDate(cronExpression);
         final long initialDelay = getInitialDelay(initialDate);
 
         final Runnable command = new Runnable() {
 
-            private OffsetDateTime nextSchedule = initialDate;
+            private ZonedDateTime nextSchedule = initialDate;
 
             @Override
             public void run() {
@@ -117,12 +117,12 @@ public class CronSchedulingAgent extends 
AbstractTimeBasedSchedulingAgent {
 
             final AtomicInteger taskNumber = new AtomicInteger(i);
 
-            final OffsetDateTime initialDate = getInitialDate(cronExpression);
+            final ZonedDateTime initialDate = getInitialDate(cronExpression);
             final long initialDelay = getInitialDelay(initialDate);
 
             final Runnable command = new Runnable() {
 
-                private OffsetDateTime nextSchedule = initialDate;
+                private ZonedDateTime nextSchedule = initialDate;
 
                 @Override
                 public void run() {
@@ -200,25 +200,32 @@ public class CronSchedulingAgent extends 
AbstractTimeBasedSchedulingAgent {
     public void setMaxThreadCount(final int maxThreads) {
     }
 
-    private OffsetDateTime getInitialDate(final CronExpression cronExpression) 
{
-        final OffsetDateTime now = OffsetDateTime.now();
-        final OffsetDateTime initialDate = cronExpression.next(now);
+    private ZonedDateTime getInitialDate(final CronExpression cronExpression) {
+        final ZonedDateTime now = ZonedDateTime.now();
+        final ZonedDateTime initialDate = cronExpression.next(now);
         return initialDate == null ? now : initialDate;
     }
 
-    private long getInitialDelay(final OffsetDateTime initialDate) {
+    private long getInitialDelay(final ZonedDateTime initialDate) {
         return initialDate.toInstant().toEpochMilli() - 
System.currentTimeMillis();
     }
 
-    private static OffsetDateTime getNextSchedule(final OffsetDateTime 
currentSchedule, final CronExpression cronExpression) {
+    private static ZonedDateTime getNextSchedule(final ZonedDateTime 
currentSchedule, final CronExpression cronExpression) {
         // Since the clock has not a millisecond precision, we have to check 
that we
         // schedule the next time after the time this was supposed to run, 
otherwise
         // we might end up with running the same task twice
-        final OffsetDateTime now = OffsetDateTime.now();
+        final ZonedDateTime now = ZonedDateTime.now(currentSchedule.getZone());
+        return getNextSchedule(currentSchedule, cronExpression, now);
+    }
+
+    static ZonedDateTime getNextSchedule(
+            final ZonedDateTime currentSchedule,
+            final CronExpression cronExpression,
+            final ZonedDateTime now) {
         return cronExpression.next(now.isAfter(currentSchedule) ? now : 
currentSchedule);
     }
 
-    private static long getDelay(final OffsetDateTime nextSchedule) {
+    private static long getDelay(final ZonedDateTime nextSchedule) {
         return Math.max(nextSchedule.toInstant().toEpochMilli() - 
System.currentTimeMillis(), 0L);
     }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/CronSchedulingAgentTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/CronSchedulingAgentTest.java
index 10a61abd38a..54dd9604761 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/CronSchedulingAgentTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/CronSchedulingAgentTest.java
@@ -30,7 +30,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.scheduling.support.CronExpression;
 
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
@@ -150,4 +155,22 @@ class CronSchedulingAgentTest {
 
         assertThrows(IllegalStateException.class, () -> 
schedulingAgent.doSchedule(reportingTaskNode, lifecycleState));
     }
+
+    @Test
+    void testNextScheduleAcrossAutumnDaylightSavingTransition() {
+        final CronExpression cronExpression = CronExpression.parse("0 30 20 ? 
* MON-FRI");
+
+        // Friday before the DST transition.
+        final LocalDateTime currentScheduleTime = LocalDateTime.of(2026, 10, 
23, 20, 30);
+        final ZoneId zone = ZoneId.of("Europe/Amsterdam");
+        final ZonedDateTime currentSchedule = currentScheduleTime.atZone(zone);
+
+        // Supply a fixed time rather than depending on the system clock.
+        final ZonedDateTime nextSchedule = 
CronSchedulingAgent.getNextSchedule(currentSchedule, cronExpression, 
currentSchedule);
+
+        final LocalDateTime expectedSchedule = LocalDateTime.of(2026, 10, 26, 
20, 30);
+        assertEquals(expectedSchedule, nextSchedule.toLocalDateTime());
+        assertEquals(ZoneOffset.ofHours(1), nextSchedule.getOffset());
+        assertEquals(zone, nextSchedule.getZone());
+    }
 }

Reply via email to