Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduler: fix Trigger#getNextFireTime() for cron-based jobs #41778

Merged
merged 1 commit into from
Jul 10, 2024
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
@@ -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,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")));
}

}

}
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,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")));
}

}

}
Loading
Loading