From 94a8a08f01748beb75bef3d36d1818db22033061 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 9 Jul 2024 11:30:59 +0200 Subject: [PATCH] Scheduler: fix Trigger#getNextFireTime() for cron-based jobs - fixes #41717 --- .../ScheduledMethodTimeZoneTest.java | 2 +- .../timezone/TriggerNextFireTimeZoneTest.java | 74 +++++++++++++++ .../timezone/TriggerPrevFireTimeZoneTest.java | 93 +++++++++++++++++++ .../quarkus/scheduler/ScheduledExecution.java | 9 ++ .../java/io/quarkus/scheduler/Trigger.java | 12 +++ .../ScheduledMethodTimeZoneTest.java | 3 +- .../timezone/TriggerNextFireTimeZoneTest.java | 74 +++++++++++++++ .../timezone/TriggerPrevFireTimeZoneTest.java | 93 +++++++++++++++++++ .../scheduler/runtime/SimpleScheduler.java | 23 +++-- 9 files changed, 371 insertions(+), 12 deletions(-) rename extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/{ => timezone}/ScheduledMethodTimeZoneTest.java (98%) create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java rename extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/{ => timezone}/ScheduledMethodTimeZoneTest.java (97%) create mode 100644 extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java create mode 100644 extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java similarity index 98% rename from extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java rename to extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java index 95d4d47724c53..38b42d218c2a6 100644 --- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java @@ -1,4 +1,4 @@ -package io.quarkus.quartz.test; +package io.quarkus.quartz.test.timezone; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java new file mode 100644 index 0000000000000..4e588223254a3 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java @@ -0,0 +1,74 @@ +package io.quarkus.quartz.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerNextFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Jobs.class); + }); + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger boston = scheduler.getScheduledJob("boston"); + Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar"); + assertNotNull(prague); + assertNotNull(boston); + assertNotNull(ulaanbaatar); + Instant pragueNext = prague.getNextFireTime(); + Instant bostonNext = boston.getNextFireTime(); + Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime(); + assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague"))); + assertTime(bostonNext.atZone(ZoneId.of("America/New_York"))); + assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + private static void assertTime(ZonedDateTime time) { + assertEquals(20, time.getHour()); + assertEquals(30, time.getMinute()); + assertEquals(0, time.getSecond()); + } + + static class Jobs { + + @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague") + void withPragueTimezone(ScheduledExecution execution) { + assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime()); + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague"))); + } + + @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York") + void withBostonTimezone() { + } + + @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar") + void withIstanbulTimezone(ScheduledExecution execution) { + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java new file mode 100644 index 0000000000000..4da6a548d7e43 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java @@ -0,0 +1,93 @@ +package io.quarkus.quartz.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerPrevFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague")); + ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul")); + // For example, the current date-time is 2024-07-09 10:08:00; + // the default time zone is Europe/London + // then the config should look like: + // simpleJobs1.cron=0/1 * 11 * * ? + // simpleJobs2.cron=0/1 * 12 * * ? + String properties = String.format( + "simpleJobs1.cron=0/1 * %s * * ?\n" + + "simpleJobs1.hour=%s\n" + + "simpleJobs2.cron=0/1 * %s * * ?\n" + + "simpleJobs2.hour=%s", + prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour()); + root.addClasses(Jobs.class) + .addAsResource( + new StringAsset(properties), + "application.properties"); + }); + + @ConfigProperty(name = "simpleJobs1.hour") + int pragueHour; + + @ConfigProperty(name = "simpleJobs2.hour") + int istanbulHour; + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS)); + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger istanbul = scheduler.getScheduledJob("istanbul"); + assertNotNull(prague); + assertNotNull(istanbul); + Instant praguePrev = prague.getPreviousFireTime(); + Instant istanbulPrev = istanbul.getPreviousFireTime(); + assertNotNull(praguePrev); + assertNotNull(istanbulPrev); + assertEquals(praguePrev, istanbulPrev); + assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour()); + assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour()); + } + + static class Jobs { + + static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1); + static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1); + + @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague") + void withPragueTimezone() { + PRAGUE_LATCH.countDown(); + } + + @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul") + void withIstanbulTimezone() { + ISTANBUL_LATCH.countDown(); + } + + } + +} diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java index 5f69240af667c..fda1ac26fec88 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java @@ -14,6 +14,9 @@ public interface ScheduledExecution { Trigger getTrigger(); /** + * The returned {@code Instant} is converted from the date-time in the default timezone. A timezone of a cron-based job + * is not taken into account. + *

* Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value. * * @return the time the associated trigger was fired @@ -21,6 +24,12 @@ public interface ScheduledExecution { Instant getFireTime(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, + * then the return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the time the action was scheduled for */ diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java index c076e5712bc0e..0a5f94d48ffb3 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java @@ -21,12 +21,24 @@ public interface Trigger { String getId(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the + * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the next time at which the trigger is scheduled to fire, or {@code null} if it will not fire again */ Instant getNextFireTime(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the + * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the previous time at which the trigger fired, or {@code null} if it has not fired yet */ diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java similarity index 97% rename from extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java rename to extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java index aba645216812a..9db547b46e7a0 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java @@ -1,4 +1,4 @@ -package io.quarkus.scheduler.test; +package io.quarkus.scheduler.test.timezone; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -41,7 +41,6 @@ public class ScheduledMethodTimeZoneTest { + "simpleJobs2.cron=0/1 * %s * * ?\n" + "simpleJobs2.timeZone=%s", now.getHour(), timeZone, job2Hour, timeZone); - // System.out.println(properties); jar.addClasses(Jobs.class) .addAsResource( new StringAsset(properties), diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java new file mode 100644 index 0000000000000..d301a0e1cff5d --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java @@ -0,0 +1,74 @@ +package io.quarkus.scheduler.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerNextFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Jobs.class); + }); + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger boston = scheduler.getScheduledJob("boston"); + Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar"); + assertNotNull(prague); + assertNotNull(boston); + assertNotNull(ulaanbaatar); + Instant pragueNext = prague.getNextFireTime(); + Instant bostonNext = boston.getNextFireTime(); + Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime(); + assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague"))); + assertTime(bostonNext.atZone(ZoneId.of("America/New_York"))); + assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + private static void assertTime(ZonedDateTime time) { + assertEquals(20, time.getHour()); + assertEquals(30, time.getMinute()); + assertEquals(0, time.getSecond()); + } + + static class Jobs { + + @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague") + void withPragueTimezone(ScheduledExecution execution) { + assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime()); + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague"))); + } + + @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York") + void withBostonTimezone() { + } + + @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar") + void withIstanbulTimezone(ScheduledExecution execution) { + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + } + +} diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java new file mode 100644 index 0000000000000..ed1ef873b77b4 --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java @@ -0,0 +1,93 @@ +package io.quarkus.scheduler.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerPrevFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague")); + ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul")); + // For example, the current date-time is 2024-07-09 10:08:00; + // the default time zone is Europe/London + // then the config should look like: + // simpleJobs1.cron=0/1 * 11 * * ? + // simpleJobs2.cron=0/1 * 12 * * ? + String properties = String.format( + "simpleJobs1.cron=0/1 * %s * * ?\n" + + "simpleJobs1.hour=%s\n" + + "simpleJobs2.cron=0/1 * %s * * ?\n" + + "simpleJobs2.hour=%s", + prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour()); + root.addClasses(Jobs.class) + .addAsResource( + new StringAsset(properties), + "application.properties"); + }); + + @ConfigProperty(name = "simpleJobs1.hour") + int pragueHour; + + @ConfigProperty(name = "simpleJobs2.hour") + int istanbulHour; + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS)); + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger istanbul = scheduler.getScheduledJob("istanbul"); + assertNotNull(prague); + assertNotNull(istanbul); + Instant praguePrev = prague.getPreviousFireTime(); + Instant istanbulPrev = istanbul.getPreviousFireTime(); + assertNotNull(praguePrev); + assertNotNull(istanbulPrev); + assertEquals(praguePrev, istanbulPrev); + assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour()); + assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour()); + } + + static class Jobs { + + static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1); + static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1); + + @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague") + void withPragueTimezone() { + PRAGUE_LATCH.countDown(); + } + + @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul") + void withIstanbulTimezone() { + ISTANBUL_LATCH.countDown(); + } + + } + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index 02e2b46e25b14..8a3d77797146e 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -589,28 +589,29 @@ static class CronTrigger extends SimpleTrigger { super(id, start, description); this.cron = cron; this.executionTime = ExecutionTime.forCron(cron); - this.lastFireTime = start; this.gracePeriod = gracePeriod; this.timeZone = timeZone; + // The last fire time stores the zoned time + this.lastFireTime = zoned(start); } @Override public Instant getNextFireTime() { - Optional nextFireTime = executionTime.nextExecution(lastFireTime); - return nextFireTime.isPresent() ? nextFireTime.get().toInstant() : null; + return executionTime.nextExecution(lastFireTime).map(ZonedDateTime::toInstant).orElse(null); } + @Override ZonedDateTime evaluate(ZonedDateTime now) { if (now.isBefore(start)) { return null; } - ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone); - Optional lastExecution = executionTime.lastExecution(zonedNow); + now = zoned(now); + Optional lastExecution = executionTime.lastExecution(now); if (lastExecution.isPresent()) { ZonedDateTime lastTruncated = lastExecution.get().truncatedTo(ChronoUnit.SECONDS); - if (zonedNow.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) { + if (now.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) { LOG.tracef("%s fired, last=%s", this, lastTruncated); - lastFireTime = zonedNow; + lastFireTime = now; return lastTruncated; } } @@ -623,9 +624,9 @@ public boolean isOverdue() { if (now.isBefore(start)) { return false; } - ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone); + now = zoned(now); Optional nextFireTime = executionTime.nextExecution(lastFireTime); - return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(zonedNow); + return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(now); } @Override @@ -634,6 +635,10 @@ public String toString() { + timeZone + "]"; } + private ZonedDateTime zoned(ZonedDateTime time) { + return timeZone == null ? time : time.withZoneSameInstant(timeZone); + } + } static class SimpleScheduledExecution implements ScheduledExecution {