Skip to content

Commit

Permalink
Scheduler: fix Trigger#getNextFireTime() for cron-based jobs
Browse files Browse the repository at this point in the history
- fixes #41717
  • Loading branch information
mkouba committed Jul 9, 2024
1 parent 1f52cf1 commit 243a4a6
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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")));
}

}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@ 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.
* <p>
* Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value.
*
* @return the time the associated trigger was fired
*/
Instant getFireTime();

/**
* If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into
* account.
* <p>
* 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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
* <p>
* 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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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")));
}

}

}
Loading

0 comments on commit 243a4a6

Please sign in to comment.