Skip to content

Commit 30af1a6

Browse files
committed
Scheduler: fix Trigger#getNextFireTime() for cron-based jobs
- fixes quarkusio#41717
1 parent 1f52cf1 commit 30af1a6

File tree

9 files changed

+371
-12
lines changed

9 files changed

+371
-12
lines changed

extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java renamed to extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.quarkus.quartz.test;
1+
package io.quarkus.quartz.test.timezone;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertTrue;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.quarkus.quartz.test.timezone;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
7+
import java.time.Instant;
8+
import java.time.ZoneId;
9+
import java.time.ZonedDateTime;
10+
11+
import jakarta.inject.Inject;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.RegisterExtension;
15+
16+
import io.quarkus.scheduler.Scheduled;
17+
import io.quarkus.scheduler.ScheduledExecution;
18+
import io.quarkus.scheduler.Scheduler;
19+
import io.quarkus.scheduler.Trigger;
20+
import io.quarkus.test.QuarkusUnitTest;
21+
22+
public class TriggerNextFireTimeZoneTest {
23+
24+
@RegisterExtension
25+
static final QuarkusUnitTest test = new QuarkusUnitTest()
26+
.withApplicationRoot(root -> {
27+
root.addClasses(Jobs.class);
28+
});
29+
30+
@Inject
31+
Scheduler scheduler;
32+
33+
@Test
34+
public void testScheduledJobs() throws InterruptedException {
35+
Trigger prague = scheduler.getScheduledJob("prague");
36+
Trigger boston = scheduler.getScheduledJob("boston");
37+
Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar");
38+
assertNotNull(prague);
39+
assertNotNull(boston);
40+
assertNotNull(ulaanbaatar);
41+
Instant pragueNext = prague.getNextFireTime();
42+
Instant bostonNext = boston.getNextFireTime();
43+
Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime();
44+
assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague")));
45+
assertTime(bostonNext.atZone(ZoneId.of("America/New_York")));
46+
assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar")));
47+
}
48+
49+
private static void assertTime(ZonedDateTime time) {
50+
assertEquals(20, time.getHour());
51+
assertEquals(30, time.getMinute());
52+
assertEquals(0, time.getSecond());
53+
}
54+
55+
static class Jobs {
56+
57+
@Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague")
58+
void withPragueTimezone(ScheduledExecution execution) {
59+
assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime());
60+
assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague")));
61+
}
62+
63+
@Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York")
64+
void withBostonTimezone() {
65+
}
66+
67+
@Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar")
68+
void withIstanbulTimezone(ScheduledExecution execution) {
69+
assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar")));
70+
}
71+
72+
}
73+
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package io.quarkus.quartz.test.timezone;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.time.Instant;
8+
import java.time.ZoneId;
9+
import java.time.ZonedDateTime;
10+
import java.util.concurrent.CountDownLatch;
11+
import java.util.concurrent.TimeUnit;
12+
13+
import jakarta.inject.Inject;
14+
15+
import org.eclipse.microprofile.config.inject.ConfigProperty;
16+
import org.jboss.shrinkwrap.api.asset.StringAsset;
17+
import org.junit.jupiter.api.Test;
18+
import org.junit.jupiter.api.extension.RegisterExtension;
19+
20+
import io.quarkus.scheduler.Scheduled;
21+
import io.quarkus.scheduler.Scheduler;
22+
import io.quarkus.scheduler.Trigger;
23+
import io.quarkus.test.QuarkusUnitTest;
24+
25+
public class TriggerPrevFireTimeZoneTest {
26+
27+
@RegisterExtension
28+
static final QuarkusUnitTest test = new QuarkusUnitTest()
29+
.withApplicationRoot(root -> {
30+
ZonedDateTime now = ZonedDateTime.now();
31+
ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague"));
32+
ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul"));
33+
// For example, the current date-time is 2024-07-09 10:08:00;
34+
// the default time zone is Europe/London
35+
// then the config should look like:
36+
// simpleJobs1.cron=0/1 * 11 * * ?
37+
// simpleJobs2.cron=0/1 * 12 * * ?
38+
String properties = String.format(
39+
"simpleJobs1.cron=0/1 * %s * * ?\n"
40+
+ "simpleJobs1.hour=%s\n"
41+
+ "simpleJobs2.cron=0/1 * %s * * ?\n"
42+
+ "simpleJobs2.hour=%s",
43+
prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour());
44+
root.addClasses(Jobs.class)
45+
.addAsResource(
46+
new StringAsset(properties),
47+
"application.properties");
48+
});
49+
50+
@ConfigProperty(name = "simpleJobs1.hour")
51+
int pragueHour;
52+
53+
@ConfigProperty(name = "simpleJobs2.hour")
54+
int istanbulHour;
55+
56+
@Inject
57+
Scheduler scheduler;
58+
59+
@Test
60+
public void testScheduledJobs() throws InterruptedException {
61+
assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS));
62+
assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS));
63+
Trigger prague = scheduler.getScheduledJob("prague");
64+
Trigger istanbul = scheduler.getScheduledJob("istanbul");
65+
assertNotNull(prague);
66+
assertNotNull(istanbul);
67+
Instant praguePrev = prague.getPreviousFireTime();
68+
Instant istanbulPrev = istanbul.getPreviousFireTime();
69+
assertNotNull(praguePrev);
70+
assertNotNull(istanbulPrev);
71+
assertEquals(praguePrev, istanbulPrev);
72+
assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour());
73+
assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour());
74+
}
75+
76+
static class Jobs {
77+
78+
static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1);
79+
static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1);
80+
81+
@Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague")
82+
void withPragueTimezone() {
83+
PRAGUE_LATCH.countDown();
84+
}
85+
86+
@Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul")
87+
void withIstanbulTimezone() {
88+
ISTANBUL_LATCH.countDown();
89+
}
90+
91+
}
92+
93+
}

extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java

+9
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@ public interface ScheduledExecution {
1414
Trigger getTrigger();
1515

1616
/**
17+
* The returned {@code Instant} is converted from the date-time in the default timezone. A timezone of a cron-based job
18+
* is not taken into account.
19+
* <p>
1720
* Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value.
1821
*
1922
* @return the time the associated trigger was fired
2023
*/
2124
Instant getFireTime();
2225

2326
/**
27+
* If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into
28+
* account.
29+
* <p>
30+
* For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin},
31+
* then the return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for
32+
* {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}.
2433
*
2534
* @return the time the action was scheduled for
2635
*/

extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java

+12
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,24 @@ public interface Trigger {
2121
String getId();
2222

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

2935
/**
36+
* If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into
37+
* account.
38+
* <p>
39+
* For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the
40+
* return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for
41+
* {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}.
3042
*
3143
* @return the previous time at which the trigger fired, or {@code null} if it has not fired yet
3244
*/

extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java renamed to extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.quarkus.scheduler.test;
1+
package io.quarkus.scheduler.test.timezone;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -41,7 +41,6 @@ public class ScheduledMethodTimeZoneTest {
4141
+ "simpleJobs2.cron=0/1 * %s * * ?\n"
4242
+ "simpleJobs2.timeZone=%s",
4343
now.getHour(), timeZone, job2Hour, timeZone);
44-
// System.out.println(properties);
4544
jar.addClasses(Jobs.class)
4645
.addAsResource(
4746
new StringAsset(properties),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.quarkus.scheduler.test.timezone;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
7+
import java.time.Instant;
8+
import java.time.ZoneId;
9+
import java.time.ZonedDateTime;
10+
11+
import jakarta.inject.Inject;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.RegisterExtension;
15+
16+
import io.quarkus.scheduler.Scheduled;
17+
import io.quarkus.scheduler.ScheduledExecution;
18+
import io.quarkus.scheduler.Scheduler;
19+
import io.quarkus.scheduler.Trigger;
20+
import io.quarkus.test.QuarkusUnitTest;
21+
22+
public class TriggerNextFireTimeZoneTest {
23+
24+
@RegisterExtension
25+
static final QuarkusUnitTest test = new QuarkusUnitTest()
26+
.withApplicationRoot(root -> {
27+
root.addClasses(Jobs.class);
28+
});
29+
30+
@Inject
31+
Scheduler scheduler;
32+
33+
@Test
34+
public void testScheduledJobs() throws InterruptedException {
35+
Trigger prague = scheduler.getScheduledJob("prague");
36+
Trigger boston = scheduler.getScheduledJob("boston");
37+
Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar");
38+
assertNotNull(prague);
39+
assertNotNull(boston);
40+
assertNotNull(ulaanbaatar);
41+
Instant pragueNext = prague.getNextFireTime();
42+
Instant bostonNext = boston.getNextFireTime();
43+
Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime();
44+
assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague")));
45+
assertTime(bostonNext.atZone(ZoneId.of("America/New_York")));
46+
assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar")));
47+
}
48+
49+
private static void assertTime(ZonedDateTime time) {
50+
assertEquals(20, time.getHour());
51+
assertEquals(30, time.getMinute());
52+
assertEquals(0, time.getSecond());
53+
}
54+
55+
static class Jobs {
56+
57+
@Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague")
58+
void withPragueTimezone(ScheduledExecution execution) {
59+
assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime());
60+
assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague")));
61+
}
62+
63+
@Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York")
64+
void withBostonTimezone() {
65+
}
66+
67+
@Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar")
68+
void withIstanbulTimezone(ScheduledExecution execution) {
69+
assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar")));
70+
}
71+
72+
}
73+
74+
}

0 commit comments

Comments
 (0)