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