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

Allow to configure Quartz misfire handling strategy #24382

Merged
merged 2 commits into from
Mar 20, 2022
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
5 changes: 5 additions & 0 deletions docs/src/main/asciidoc/quartz.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

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

}
}
Original file line number Diff line number Diff line change
@@ -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() {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.quartz.runtime;

import java.util.EnumSet;
import java.util.Locale;

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;

String dashedName() {
return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
}

static EnumSet<QuartzMisfirePolicy> validCronValues() {
return EnumSet.of(SMART_POLICY, IGNORE_MISFIRE_POLICY, FIRE_NOW, CRON_TRIGGER_DO_NOTHING);
}

static EnumSet<QuartzMisfirePolicy> validSimpleValues() {
return EnumSet.of(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,
SIMPLE_TRIGGER_RESCHEDULE_NEXT_WITH_REMAINING_COUNT);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<String, QuartzMisfirePolicyConfig> misfirePolicyPerJobs;

@ConfigGroup
public static class QuartzMisfirePolicyConfig {
/**
* The quartz misfire policy for this job.
*/
@ConfigItem(defaultValue = "smart-policy", name = ConfigItem.PARENT)
public QuartzMisfirePolicy misfirePolicy;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Objects;
import java.util.OptionalLong;
import java.util.Properties;
import java.util.stream.Collectors;

import javax.annotation.PreDestroy;
import javax.annotation.Priority;
Expand Down Expand Up @@ -154,7 +155,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)) {
Expand All @@ -174,15 +176,77 @@ public QuartzScheduler(SchedulerContext context, QuartzSupport quartzSupport, Sc
break;
}
}
scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
if (perJobConfig != null) {
switch (perJobConfig.misfirePolicy) {
case SMART_POLICY:
// this is the default, doing nothing
break;
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("Cron job " + identity
+ " configured with invalid misfire policy "
+ perJobConfig.misfirePolicy.dashedName() +
"\nValid options are: "
+ QuartzMisfirePolicy.validCronValues().stream()
.map(QuartzMisfirePolicy::dashedName)
.collect(Collectors.joining(", ")));
}
}
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 SMART_POLICY:
// this is the default, doing nothing
break;
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("Simple job " + identity
+ " configured with invalid misfire policy "
+ perJobConfig.misfirePolicy.dashedName() +
"\nValid options are: "
+ QuartzMisfirePolicy.validSimpleValues().stream()
.map(QuartzMisfirePolicy::dashedName)
.collect(Collectors.joining(", ")));
}
}
scheduleBuilder = simpleScheduleBuilder;
} else {
throw new IllegalArgumentException("Invalid schedule configuration: " + scheduled);
}
Expand Down