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 95d4d47724c53b..38b42d218c2a67 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 00000000000000..3d1eb5d6bb6cf9 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java @@ -0,0 +1,77 @@ +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 static org.junit.jupiter.api.Assertions.assertTrue; + +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(); + assertTrue(ulaanbaatarNext.isBefore(pragueNext)); + assertTrue(pragueNext.isBefore(bostonNext)); + 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 withLondonTimezone() { + } + + @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 00000000000000..4da6a548d7e439 --- /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 5f69240af667cf..fda1ac26fec881 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 c076e5712bc0eb..0a5f94d48ffb30 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 aba645216812ac..9db547b46e7a06 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 00000000000000..304f5445f7b0a1
--- /dev/null
+++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java
@@ -0,0 +1,77 @@
+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 static org.junit.jupiter.api.Assertions.assertTrue;
+
+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();
+ assertTrue(ulaanbaatarNext.isBefore(pragueNext));
+ assertTrue(pragueNext.isBefore(bostonNext));
+ 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 withLondonTimezone() {
+ }
+
+ @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 00000000000000..ed1ef873b77b47
--- /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 02e2b46e25b142..c1f5789fd77828 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
@@ -596,8 +596,29 @@ static class CronTrigger extends SimpleTrigger {
@Override
public Instant getNextFireTime() {
- Optional