Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
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;
Expand Down Expand Up @@ -67,12 +67,12 @@ public void doSchedule(final ReportingTaskNode taskNode, final LifecycleState sc

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() {
Expand Down Expand Up @@ -117,12 +117,12 @@ public synchronized void doSchedule(final Connectable connectable, final Lifecyc

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() {
Expand Down Expand Up @@ -200,25 +200,32 @@ public void onEvent(final Connectable connectable) {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
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;

Expand Down Expand Up @@ -150,4 +155,22 @@ void testDoScheduleReportingTaskNodeCronExpressionInvalid() {

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