diff --git a/docs/src/main/asciidoc/quartz.adoc b/docs/src/main/asciidoc/quartz.adoc index 0dc7bacf2a0c4..d32a86cb0be95 100644 --- a/docs/src/main/asciidoc/quartz.adoc +++ b/docs/src/main/asciidoc/quartz.adoc @@ -201,6 +201,7 @@ Edit the `application.properties` file and add the below configuration: # Quartz configuration quarkus.quartz.clustered=true <1> quarkus.quartz.store-type=jdbc-cmt <2> +quarkus.quartz.misfire-policy.task-job=ignore-misfire-policy <3> # Datasource configuration. quarkus.datasource.db-kind=postgresql @@ -223,6 +224,10 @@ quarkus.flyway.baseline-description=Quartz ---- <1> Indicate that the scheduler will be run in clustered mode <2> Use the database store to persist job related information so that they can be shared between nodes +<3> The misfire policy can be configured for each job. `task-job` is the identity of the job. + +Valid misfire policy for cron jobs are: `smart-policy`, `ignore-misfire-policy`, `fire-now` and `cron-trigger-do-nothing`. +Valid misfire policy for interval jobs are: `smart-policy`, `ignore-misfire-policy`, `fire-now`, `simple-trigger-reschedule-now-with-existing-repeat-count`, `simple-trigger-reschedule-now-with-remaining-repeat-count`, `simple-trigger-reschedule-next-with-existing-count` and `simple-trigger-reschedule-next-with-remaining-count`. == Creating a REST resource and a test diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/InvalidMisfirePolicyTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/InvalidMisfirePolicyTest.java new file mode 100644 index 0000000000000..9c719b9c34ef6 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/InvalidMisfirePolicyTest.java @@ -0,0 +1,39 @@ +package io.quarkus.quartz.test; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidMisfirePolicyTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .setExpectedException(IllegalArgumentException.class) + .withApplicationRoot((jar) -> jar + .addClasses(MisfirePolicyTest.Jobs.class) + .addAsResource(new StringAsset( + "quarkus.quartz.misfire-policy.\"simple_invalid_misfire_policy\"=cron-trigger-do-nothing\n" + + "quarkus.quartz.misfire-policy.\"cron_invalid_misfire_policy\"=simple-trigger-reschedule-now-with-existing-repeat-count\n"), + "application.properties")); + + @Test + public void shouldFailWhenInvalidMisfirePolicyConfiguration() { + Assertions.fail(); + } + + static class Jobs { + + @Scheduled(identity = "simple_invalid_misfire_policy", every = "1s") + void simpleInvalidMisfirePolicy() { + } + + @Scheduled(identity = "cron_invalid_misfire_policy", cron = "0/1 * * * * ?") + void cronInvalidMisfirePolicy() { + } + + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/MisfirePolicyTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/MisfirePolicyTest.java new file mode 100644 index 0000000000000..07920b8aea62f --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/MisfirePolicyTest.java @@ -0,0 +1,136 @@ +package io.quarkus.quartz.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.quartz.CronTrigger; +import org.quartz.SchedulerException; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; +import org.quartz.TriggerKey; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.test.QuarkusUnitTest; + +public class MisfirePolicyTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Jobs.class) + .addAsResource(new StringAsset( + "quarkus.quartz.misfire-policy.\"simple_ignore_misfire_policy\"=ignore-misfire-policy\n" + + "quarkus.quartz.misfire-policy.\"cron_ignore_misfire_policy\"=ignore-misfire-policy\n" + + "quarkus.quartz.misfire-policy.\"simple_reschedule_now_existing_policy\"=simple-trigger-reschedule-now-with-existing-repeat-count\n" + + + "quarkus.quartz.misfire-policy.\"simple_reschedule_now_remaining_policy\"=simple-trigger-reschedule-now-with-remaining-repeat-count\n" + + + "quarkus.quartz.misfire-policy.\"simple_reschedule_next_existing_policy\"=simple-trigger-reschedule-next-with-existing-count\n" + + + "quarkus.quartz.misfire-policy.\"simple_reschedule_next_remaining_policy\"=simple-trigger-reschedule-next-with-remaining-count\n" + + + "quarkus.quartz.misfire-policy.\"cron_do_nothing_policy\"=cron-trigger-do-nothing\n" + + ), "application.properties")); + + @Inject + org.quartz.Scheduler quartz; + + @Test + public void testDefaultMisfirePolicy() throws SchedulerException { + Trigger defaultMisfirePolicy = quartz + .getTrigger(new TriggerKey("default_misfire_policy", Scheduler.class.getName())); + assertNotNull(defaultMisfirePolicy); + assertEquals(Trigger.MISFIRE_INSTRUCTION_SMART_POLICY, defaultMisfirePolicy.getMisfireInstruction()); + + } + + @Test + public void testIgnoreMisfirePolicy() throws SchedulerException { + Trigger simpleIgnoreMisfirePolicyTrigger = quartz + .getTrigger(new TriggerKey("simple_ignore_misfire_policy", Scheduler.class.getName())); + assertNotNull(simpleIgnoreMisfirePolicyTrigger); + assertEquals(Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY, + simpleIgnoreMisfirePolicyTrigger.getMisfireInstruction()); + + Trigger cronIgnoreMisfirePolicyTrigger = quartz + .getTrigger(new TriggerKey("cron_ignore_misfire_policy", Scheduler.class.getName())); + assertNotNull(cronIgnoreMisfirePolicyTrigger); + assertEquals(Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY, cronIgnoreMisfirePolicyTrigger.getMisfireInstruction()); + } + + @Test + public void testSimpleTriggerMisfirePolicy() throws SchedulerException { + Trigger simpleRescheduleNowExistingPolicy = quartz + .getTrigger(new TriggerKey("simple_reschedule_now_existing_policy", Scheduler.class.getName())); + assertNotNull(simpleRescheduleNowExistingPolicy); + assertEquals(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT, + simpleRescheduleNowExistingPolicy.getMisfireInstruction()); + + Trigger simpleRescheduleNowRemainingPolicy = quartz + .getTrigger(new TriggerKey("simple_reschedule_now_remaining_policy", Scheduler.class.getName())); + assertNotNull(simpleRescheduleNowRemainingPolicy); + assertEquals(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT, + simpleRescheduleNowRemainingPolicy.getMisfireInstruction()); + + Trigger simpleRescheduleNextExistingPolicy = quartz + .getTrigger(new TriggerKey("simple_reschedule_next_existing_policy", Scheduler.class.getName())); + assertNotNull(simpleRescheduleNextExistingPolicy); + assertEquals(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT, + simpleRescheduleNextExistingPolicy.getMisfireInstruction()); + + Trigger simpleRescheduleNextRemainingPolicy = quartz + .getTrigger(new TriggerKey("simple_reschedule_next_remaining_policy", Scheduler.class.getName())); + assertNotNull(simpleRescheduleNextRemainingPolicy); + assertEquals(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT, + simpleRescheduleNextRemainingPolicy.getMisfireInstruction()); + } + + @Test + public void testCronTriggerMisfirePolicy() throws SchedulerException { + Trigger cronDoNothingPolicy = quartz.getTrigger(new TriggerKey("cron_do_nothing_policy", Scheduler.class.getName())); + assertNotNull(cronDoNothingPolicy); + assertEquals(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING, cronDoNothingPolicy.getMisfireInstruction()); + } + + static class Jobs { + + @Scheduled(identity = "default_misfire_policy", every = "1s") + void defaultMisfirePolicy() { + } + + @Scheduled(identity = "simple_ignore_misfire_policy", every = "1s") + void simpleIgnoreMisfirePolicy() { + } + + @Scheduled(identity = "cron_ignore_misfire_policy", cron = "0/1 * * * * ?") + void cronIgnoreMisfirePolicy() { + } + + @Scheduled(identity = "simple_reschedule_now_existing_policy", every = "1s") + void simpleRescheduleNowExistingPolicy() { + } + + @Scheduled(identity = "simple_reschedule_now_remaining_policy", every = "1s") + void simpleRescheduleNowRemainingPolicy() { + } + + @Scheduled(identity = "simple_reschedule_next_existing_policy", every = "1s") + void simpleRescheduleNextExistingPolicy() { + } + + @Scheduled(identity = "simple_reschedule_next_remaining_policy", every = "1s") + void simpleRescheduleNextRemainingPolicy() { + } + + @Scheduled(identity = "cron_do_nothing_policy", cron = "0/1 * * * * ?") + void cronDoNothingPolicy() { + } + } +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzMisfirePolicy.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzMisfirePolicy.java new file mode 100644 index 0000000000000..2ac280057a23f --- /dev/null +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzMisfirePolicy.java @@ -0,0 +1,12 @@ +package io.quarkus.quartz.runtime; + +public enum QuartzMisfirePolicy { + SMART_POLICY, + IGNORE_MISFIRE_POLICY, + FIRE_NOW, + SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT, + SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT, + SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_REMAINING_COUNT, + SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_EXISTING_COUNT, + CRON_TRIGGER_DO_NOTHING +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java index 76d76d096eae0..8bc0a4be4842e 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java @@ -1,7 +1,11 @@ package io.quarkus.quartz.runtime; import java.time.Duration; +import java.util.Map; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -45,4 +49,22 @@ public class QuartzRuntimeConfig { */ @ConfigItem(defaultValue = "normal") public QuartzStartMode startMode; + + /** + * Misfire policy per job configuration. + */ + @ConfigDocSection + @ConfigDocMapKey("identity") + @ConfigItem(name = "misfire-policy") + public Map misfirePolicyPerJobs; + + @ConfigGroup + public static class QuartzMisfirePolicyConfig { + /** + * The quartz misfire policy for this job. + */ + @ConfigItem(defaultValue = "smart-policy", name = ConfigItem.PARENT) + public QuartzMisfirePolicy misfirePolicy; + } + } diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java index af33481f128ce..0f07ff956459b 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzScheduler.java @@ -154,7 +154,8 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc .usingJobData(INVOKER_KEY, method.getInvokerClassName()) .requestRecovery(); ScheduleBuilder scheduleBuilder; - + QuartzRuntimeConfig.QuartzMisfirePolicyConfig perJobConfig = runtimeConfig.misfirePolicyPerJobs + .get(identity); String cron = SchedulerUtils.lookUpPropertyValue(scheduled.cron()); if (!cron.isEmpty()) { if (SchedulerUtils.isOff(cron)) { @@ -174,15 +175,64 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc break; } } - scheduleBuilder = CronScheduleBuilder.cronSchedule(cron); + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron); + if (perJobConfig != null) { + switch (perJobConfig.misfirePolicy) { + case IGNORE_MISFIRE_POLICY: + cronScheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires(); + break; + case FIRE_NOW: + cronScheduleBuilder.withMisfireHandlingInstructionFireAndProceed(); + break; + case CRON_TRIGGER_DO_NOTHING: + cronScheduleBuilder.withMisfireHandlingInstructionDoNothing(); + break; + case SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT: + case SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT: + case SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_EXISTING_COUNT: + case SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_REMAINING_COUNT: + throw new IllegalArgumentException("configured cron job " + identity + + " with invalid misfire policy " + perJobConfig.misfirePolicy + + "\nvalid options are: IGNORE_MISFIRE_POLICY, FIRE_NOW, CRON_TRIGGER_DO_NOTHING"); + + } + } + scheduleBuilder = cronScheduleBuilder; } else if (!scheduled.every().isEmpty()) { OptionalLong everyMillis = SchedulerUtils.parseEveryAsMillis(scheduled); if (!everyMillis.isPresent()) { continue; } - scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInMilliseconds(everyMillis.getAsLong()) .repeatForever(); + if (perJobConfig != null) { + switch (perJobConfig.misfirePolicy) { + case IGNORE_MISFIRE_POLICY: + simpleScheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires(); + break; + case FIRE_NOW: + simpleScheduleBuilder.withMisfireHandlingInstructionFireNow(); + break; + case SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT: + simpleScheduleBuilder.withMisfireHandlingInstructionNowWithExistingCount(); + break; + case SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT: + simpleScheduleBuilder.withMisfireHandlingInstructionNowWithRemainingCount(); + break; + case SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_EXISTING_COUNT: + simpleScheduleBuilder.withMisfireHandlingInstructionNextWithExistingCount(); + break; + case SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_REMAINING_COUNT: + simpleScheduleBuilder.withMisfireHandlingInstructionNextWithRemainingCount(); + break; + case CRON_TRIGGER_DO_NOTHING: + throw new IllegalArgumentException("configured simple job " + identity + + " with invalid misfire policy CRON_TRIGGER_DO_NOTHING" + + "\nvalid options are: IGNORE_MISFIRE_POLICY, FIRE_NOW, SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT, SIMPLE_TRIGGER_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT, SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_EXISTING_COUNT and SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_REMAINING_COUNT"); + } + } + scheduleBuilder = simpleScheduleBuilder; } else { throw new IllegalArgumentException("Invalid schedule configuration: " + scheduled); }