io.quarkus.quartz.runtime.QuartzSchedulerImpl Maven / Gradle / Ivy
package io.quarkus.quartz.runtime;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Properties;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.BeforeDestroyed;
import jakarta.enterprise.event.Event;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Reception;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.Typed;
import jakarta.inject.Singleton;
import jakarta.interceptor.Interceptor;
import jakarta.transaction.SystemException;
import jakarta.transaction.UserTransaction;
import org.jboss.logging.Logger;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.ScheduleBuilder;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.simpl.InitThreadContextClassLoadHelper;
import org.quartz.simpl.SimpleJobFactory;
import org.quartz.spi.TriggerFiredBundle;
import com.cronutils.mapper.CronMapper;
import com.cronutils.model.Cron;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinition;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.parser.CronParser;
import io.quarkus.arc.Subclass;
import io.quarkus.quartz.QuartzScheduler;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.scheduler.FailedExecution;
import io.quarkus.scheduler.Scheduled;
import io.quarkus.scheduler.ScheduledExecution;
import io.quarkus.scheduler.ScheduledJobPaused;
import io.quarkus.scheduler.ScheduledJobResumed;
import io.quarkus.scheduler.Scheduler;
import io.quarkus.scheduler.SchedulerPaused;
import io.quarkus.scheduler.SchedulerResumed;
import io.quarkus.scheduler.SkippedExecution;
import io.quarkus.scheduler.SuccessfulExecution;
import io.quarkus.scheduler.Trigger;
import io.quarkus.scheduler.common.runtime.AbstractJobDefinition;
import io.quarkus.scheduler.common.runtime.DefaultInvoker;
import io.quarkus.scheduler.common.runtime.Events;
import io.quarkus.scheduler.common.runtime.ScheduledInvoker;
import io.quarkus.scheduler.common.runtime.ScheduledMethod;
import io.quarkus.scheduler.common.runtime.SchedulerContext;
import io.quarkus.scheduler.common.runtime.SyntheticScheduled;
import io.quarkus.scheduler.common.runtime.util.SchedulerUtils;
import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig;
import io.quarkus.scheduler.runtime.SchedulerRuntimeConfig.StartMode;
import io.quarkus.scheduler.runtime.SimpleScheduler;
import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle;
import io.quarkus.virtual.threads.VirtualThreadsRecorder;
import io.smallrye.common.vertx.VertxContext;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
/**
* Although this class is not part of the public API it must not be renamed in order to preserve backward compatibility. The
* name of this class can be stored in a Quartz table in the database. See https://github.com/quarkusio/quarkus/issues/29177
* for more information.
*/
@Typed({ QuartzScheduler.class, Scheduler.class })
@Singleton
public class QuartzSchedulerImpl implements QuartzScheduler {
private static final Logger LOGGER = Logger.getLogger(QuartzSchedulerImpl.class.getName());
private static final String INVOKER_KEY = "invoker";
private final org.quartz.Scheduler scheduler;
private final boolean startHalted;
private final Duration shutdownWaitTime;
private final boolean enabled;
private final CronType cronType;
private final CronParser cronParser;
private final Duration defaultOverdueGracePeriod;
private final Map scheduledTasks = new ConcurrentHashMap<>();
private final Event skippedExecutionEvent;
private final Event successExecutionEvent;
private final Event failedExecutionEvent;
private final Event schedulerPausedEvent;
private final Event schedulerResumedEvent;
private final Event scheduledJobPausedEvent;
private final Event scheduledJobResumedEvent;
private final QuartzRuntimeConfig runtimeConfig;
public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport,
SchedulerRuntimeConfig schedulerRuntimeConfig,
Event skippedExecutionEvent, Event successExecutionEvent,
Event failedExecutionEvent, Event schedulerPausedEvent,
Event schedulerResumedEvent, Event scheduledJobPausedEvent,
Event scheduledJobResumedEvent,
Instance jobs, Instance userTransaction,
Vertx vertx) {
this.shutdownWaitTime = quartzSupport.getRuntimeConfig().shutdownWaitTime;
this.skippedExecutionEvent = skippedExecutionEvent;
this.successExecutionEvent = successExecutionEvent;
this.failedExecutionEvent = failedExecutionEvent;
this.schedulerPausedEvent = schedulerPausedEvent;
this.schedulerResumedEvent = schedulerResumedEvent;
this.scheduledJobPausedEvent = scheduledJobPausedEvent;
this.scheduledJobResumedEvent = scheduledJobResumedEvent;
this.runtimeConfig = quartzSupport.getRuntimeConfig();
this.enabled = schedulerRuntimeConfig.enabled;
this.defaultOverdueGracePeriod = schedulerRuntimeConfig.overdueGracePeriod;
StartMode startMode = initStartMode(schedulerRuntimeConfig, runtimeConfig);
boolean forceStart;
if (startMode != StartMode.NORMAL) {
startHalted = (startMode == StartMode.HALTED);
forceStart = startHalted || (startMode == StartMode.FORCED);
} else {
startHalted = false;
forceStart = false;
}
var simpleTriggerConfig = runtimeConfig.simpleTriggerConfig;
var cronTriggerConfig = runtimeConfig.cronTriggerConfig;
if (!QuartzMisfirePolicy.validCronValues().contains(cronTriggerConfig.misfirePolicyConfig.misfirePolicy)) {
throw new IllegalArgumentException(
"Global cron trigger misfire policy configured with invalid option. Valid options are: "
+ QuartzMisfirePolicy.validCronValues().stream()
.map(QuartzMisfirePolicy::dashedName)
.collect(Collectors.joining(", ")));
}
if (!QuartzMisfirePolicy.validSimpleValues().contains(simpleTriggerConfig.misfirePolicyConfig.misfirePolicy)) {
throw new IllegalArgumentException(
"Global simple trigger misfire policy configured with invalid option. Valid options are: "
+ QuartzMisfirePolicy.validSimpleValues().stream()
.map(QuartzMisfirePolicy::dashedName)
.collect(Collectors.joining(", ")));
}
cronType = context.getCronType();
CronDefinition def = CronDefinitionBuilder.instanceDefinitionFor(cronType);
cronParser = new CronParser(def);
if (!enabled) {
LOGGER.info("Quartz scheduler is disabled by config property and will not be started");
this.scheduler = null;
} else if (!forceStart && context.getScheduledMethods().isEmpty()) {
LOGGER.info("No scheduled business methods found - Quartz scheduler will not be started");
this.scheduler = null;
} else {
UserTransaction transaction = null;
try {
boolean manageTx = quartzSupport.getBuildTimeConfig().storeType.isNonManagedTxJobStore();
if (manageTx && userTransaction.isResolvable()) {
transaction = userTransaction.get();
}
Properties props = getSchedulerConfigurationProperties(quartzSupport);
SchedulerFactory schedulerFactory = new StdSchedulerFactory(props);
scheduler = schedulerFactory.getScheduler();
// Set custom job factory
scheduler.setJobFactory(new InvokerJobFactory(scheduledTasks, jobs, vertx));
if (transaction != null) {
transaction.begin();
}
for (ScheduledMethod method : context.getScheduledMethods()) {
int nameSequence = 0;
for (Scheduled scheduled : method.getSchedules()) {
String identity = SchedulerUtils.lookUpPropertyValue(scheduled.identity());
if (identity.isEmpty()) {
identity = ++nameSequence + "_" + method.getInvokerClassName();
}
ScheduledInvoker invoker = SimpleScheduler.initInvoker(
context.createInvoker(method.getInvokerClassName()),
skippedExecutionEvent, successExecutionEvent, failedExecutionEvent,
scheduled.concurrentExecution(),
SimpleScheduler.initSkipPredicate(scheduled.skipExecutionIf()));
JobDetail jobDetail = createJobDetail(identity, method.getInvokerClassName());
Optional> triggerBuilder = createTrigger(identity, scheduled, cronType, runtimeConfig,
jobDetail);
if (triggerBuilder.isPresent()) {
org.quartz.Trigger trigger = triggerBuilder.get().build();
org.quartz.Trigger oldTrigger = scheduler.getTrigger(trigger.getKey());
if (oldTrigger != null) {
trigger = triggerBuilder.get().startAt(oldTrigger.getNextFireTime()).build();
scheduler.rescheduleJob(trigger.getKey(), trigger);
LOGGER.debugf("Rescheduled business method %s with config %s", method.getMethodDescription(),
scheduled);
} else if (!scheduler.checkExists(trigger.getKey())) {
scheduler.scheduleJob(jobDetail, trigger);
LOGGER.debugf("Scheduled business method %s with config %s", method.getMethodDescription(),
scheduled);
} else {
// TODO remove this code in 3.0, it is only here to ensure migration after the removal of
// "_trigger" suffix and the need to reschedule jobs due to configuration change between build
// and deploy time
oldTrigger = scheduler
.getTrigger(new TriggerKey(identity + "_trigger", Scheduler.class.getName()));
if (oldTrigger != null) {
scheduler.deleteJob(jobDetail.getKey());
trigger = triggerBuilder.get().startAt(oldTrigger.getNextFireTime()).build();
scheduler.scheduleJob(jobDetail, trigger);
LOGGER.debugf(
"Rescheduled business method %s with config %s due to Trigger '%s' record being renamed after removal of '_trigger' suffix",
method.getMethodDescription(),
scheduled, oldTrigger.getKey().getName());
}
}
scheduledTasks.put(identity, new QuartzTrigger(trigger.getKey(),
new Function<>() {
@Override
public org.quartz.Trigger apply(TriggerKey triggerKey) {
try {
return scheduler.getTrigger(triggerKey);
} catch (SchedulerException e) {
throw new IllegalStateException(e);
}
}
}, invoker,
SchedulerUtils.parseOverdueGracePeriod(scheduled, defaultOverdueGracePeriod),
quartzSupport.getRuntimeConfig().runBlockingScheduledMethodOnQuartzThread, false,
method.getMethodDescription()));
}
}
}
if (transaction != null) {
transaction.commit();
}
} catch (Throwable e) {
if (transaction != null) {
try {
transaction.rollback();
} catch (SystemException ex) {
LOGGER.error("Unable to rollback transaction", ex);
}
}
throw new IllegalStateException("Unable to create Scheduler", e);
}
}
}
@Produces
@Singleton
org.quartz.Scheduler produceQuartzScheduler() {
if (scheduler == null) {
throw new IllegalStateException(
"Quartz scheduler is either explicitly disabled through quarkus.scheduler.enabled=false or no @Scheduled methods were found. If you only need to schedule a job programmatically you can force the start of the scheduler by setting 'quarkus.scheduler.start-mode=forced'.");
}
return scheduler;
}
@Override
public org.quartz.Scheduler getScheduler() {
return scheduler;
}
@Override
public void pause() {
if (!enabled) {
LOGGER.warn("Quartz Scheduler is disabled and cannot be paused");
} else {
try {
if (scheduler != null) {
scheduler.standby();
Events.fire(schedulerPausedEvent, SchedulerPaused.INSTANCE);
}
} catch (SchedulerException e) {
throw new RuntimeException("Unable to pause scheduler", e);
}
}
}
@Override
public void pause(String identity) {
Objects.requireNonNull(identity, "Cannot pause - identity is null");
if (identity.isEmpty()) {
LOGGER.warn("Cannot pause - identity is empty");
return;
}
try {
String parsedIdentity = SchedulerUtils.lookUpPropertyValue(identity);
QuartzTrigger trigger = scheduledTasks.get(parsedIdentity);
if (trigger != null) {
scheduler.pauseJob(new JobKey(parsedIdentity, Scheduler.class.getName()));
Events.fire(scheduledJobPausedEvent, new ScheduledJobPaused(trigger));
}
} catch (SchedulerException e) {
throw new RuntimeException("Unable to pause job", e);
}
}
@Override
public boolean isPaused(String identity) {
Objects.requireNonNull(identity);
if (identity.isEmpty()) {
return false;
}
try {
List extends org.quartz.Trigger> triggers = scheduler
.getTriggersOfJob(new JobKey(SchedulerUtils.lookUpPropertyValue(identity), Scheduler.class.getName()));
if (triggers.isEmpty()) {
return false;
}
for (org.quartz.Trigger trigger : triggers) {
try {
if (scheduler.getTriggerState(trigger.getKey()) != TriggerState.PAUSED) {
return false;
}
} catch (SchedulerException e) {
LOGGER.warnf("Cannot obtain the trigger state for %s", trigger.getKey());
return false;
}
}
return true;
} catch (SchedulerException e1) {
LOGGER.warnf(e1, "Cannot obtain triggers for job with identity %s", identity);
return false;
}
}
@Override
public void resume() {
if (!enabled) {
LOGGER.warn("Quartz Scheduler is disabled and cannot be resumed");
} else {
try {
if (scheduler != null) {
scheduler.start();
Events.fire(schedulerResumedEvent, SchedulerResumed.INSTANCE);
}
} catch (SchedulerException e) {
throw new RuntimeException("Unable to resume scheduler", e);
}
}
}
@Override
public void resume(String identity) {
Objects.requireNonNull(identity, "Cannot resume - identity is null");
if (identity.isEmpty()) {
LOGGER.warn("Cannot resume - identity is empty");
return;
}
try {
String parsedIdentity = SchedulerUtils.lookUpPropertyValue(identity);
QuartzTrigger trigger = scheduledTasks.get(parsedIdentity);
if (trigger != null) {
scheduler.resumeJob(new JobKey(SchedulerUtils.lookUpPropertyValue(parsedIdentity), Scheduler.class.getName()));
Events.fire(scheduledJobResumedEvent, new ScheduledJobResumed(trigger));
}
} catch (SchedulerException e) {
throw new RuntimeException("Unable to resume job", e);
}
}
@Override
public boolean isRunning() {
if (!enabled || scheduler == null) {
return false;
} else {
try {
return !scheduler.isInStandbyMode();
} catch (SchedulerException e) {
throw new IllegalStateException("Could not evaluate standby mode", e);
}
}
}
@Override
public List getScheduledJobs() {
return List.copyOf(scheduledTasks.values());
}
@Override
public Trigger getScheduledJob(String identity) {
Objects.requireNonNull(identity);
if (identity.isEmpty()) {
return null;
}
return scheduledTasks.get(SchedulerUtils.lookUpPropertyValue(identity));
}
@Override
public JobDefinition newJob(String identity) {
Objects.requireNonNull(identity);
if (scheduledTasks.containsKey(identity)) {
throw new IllegalStateException("A job with this identity is already scheduled: " + identity);
}
return new QuartzJobDefinition(identity);
}
@Override
public Trigger unscheduleJob(String identity) {
Objects.requireNonNull(identity);
if (!identity.isEmpty()) {
String parsedIdentity = SchedulerUtils.lookUpPropertyValue(identity);
QuartzTrigger trigger = scheduledTasks.get(parsedIdentity);
if (trigger != null && trigger.isProgrammatic) {
if (scheduledTasks.remove(identity) != null) {
try {
scheduler.unscheduleJob(trigger.triggerKey);
} catch (SchedulerException e) {
throw new IllegalStateException("Unable to unschedule job with identity: " + identity);
}
return trigger;
}
}
}
return null;
}
// Use Interceptor.Priority.PLATFORM_BEFORE to start the scheduler before regular StartupEvent observers
void start(@Observes @Priority(Interceptor.Priority.PLATFORM_BEFORE) StartupEvent startupEvent) {
if (scheduler == null || startHalted) {
return;
}
try {
scheduler.start();
} catch (SchedulerException e) {
throw new IllegalStateException("Unable to start Scheduler", e);
}
}
/**
* Need to gracefully shut down the scheduler making sure that all triggers have been
* released before datasource shutdown.
*
* @param event ignored
*/
void destroy(@Observes(notifyObserver = Reception.IF_EXISTS) @BeforeDestroyed(ApplicationScoped.class) Object event) {
if (scheduler != null) {
try {
if (shutdownWaitTime.isZero()) {
scheduler.shutdown(false);
} else {
CompletableFuture.supplyAsync(new Supplier<>() {
@Override
public Void get() {
// Note that this method does not return until all currently executing jobs have completed
try {
scheduler.shutdown(true);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
return null;
}
}).get(shutdownWaitTime.toMillis(), TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
LOGGER.warnf("Unable to gracefully shutdown the scheduler", e);
}
}
}
@PreDestroy
void destroy() {
if (scheduler != null) {
try {
if (!scheduler.isShutdown()) {
scheduler.shutdown(false); // force shutdown
}
} catch (SchedulerException e) {
LOGGER.warnf("Unable to shutdown the scheduler", e);
}
}
}
private Properties getSchedulerConfigurationProperties(QuartzSupport quartzSupport) {
Properties props = new Properties();
QuartzBuildTimeConfig buildTimeConfig = quartzSupport.getBuildTimeConfig();
QuartzRuntimeConfig runtimeConfig = quartzSupport.getRuntimeConfig();
props.put("org.quartz.scheduler.skipUpdateCheck", "true");
props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, runtimeConfig.instanceName);
props.put(StdSchedulerFactory.PROP_SCHED_BATCH_TIME_WINDOW, runtimeConfig.batchTriggerAcquisitionFireAheadTimeWindow);
props.put(StdSchedulerFactory.PROP_SCHED_MAX_BATCH_SIZE, runtimeConfig.batchTriggerAcquisitionMaxCount);
props.put(StdSchedulerFactory.PROP_SCHED_WRAP_JOB_IN_USER_TX, "false");
props.put(StdSchedulerFactory.PROP_SCHED_SCHEDULER_THREADS_INHERIT_CONTEXT_CLASS_LOADER_OF_INITIALIZING_THREAD, "true");
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
props.put(StdSchedulerFactory.PROP_SCHED_CLASS_LOAD_HELPER_CLASS, InitThreadContextClassLoadHelper.class.getName());
props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", "" + runtimeConfig.threadCount);
props.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadPriority", "" + runtimeConfig.threadPriority);
props.put(StdSchedulerFactory.PROP_SCHED_RMI_EXPORT, "false");
props.put(StdSchedulerFactory.PROP_SCHED_RMI_PROXY, "false");
props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, buildTimeConfig.storeType.clazz);
if (buildTimeConfig.storeType.isDbStore()) {
String dataSource = buildTimeConfig.dataSourceName.orElse("QUARKUS_QUARTZ_DEFAULT_DATASOURCE");
QuarkusQuartzConnectionPoolProvider.setDataSourceName(dataSource);
boolean serializeJobData = buildTimeConfig.serializeJobData.orElse(false);
props.put(StdSchedulerFactory.PROP_JOB_STORE_USE_PROP, serializeJobData ? "false" : "true");
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".misfireThreshold",
"" + runtimeConfig.misfireThreshold.toMillis());
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".tablePrefix", buildTimeConfig.tablePrefix);
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".dataSource", dataSource);
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".driverDelegateClass",
quartzSupport.getDriverDialect().get());
props.put(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dataSource + ".connectionProvider.class",
QuarkusQuartzConnectionPoolProvider.class.getName());
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".acquireTriggersWithinLock", "true");
if (buildTimeConfig.clustered) {
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".isClustered", "true");
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".clusterCheckinInterval",
"" + buildTimeConfig.clusterCheckinInterval);
if (buildTimeConfig.selectWithLockSql.isPresent()) {
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".selectWithLockSQL",
buildTimeConfig.selectWithLockSql.get());
}
}
if (buildTimeConfig.storeType.isNonManagedTxJobStore()) {
props.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".nonManagedTXDataSource", dataSource);
}
}
QuartzExtensionPointConfig instanceIdGenerator = buildTimeConfig.instanceIdGenerators.get(runtimeConfig.instanceId);
if (runtimeConfig.instanceId.equals(StdSchedulerFactory.AUTO_GENERATE_INSTANCE_ID) || instanceIdGenerator != null) {
props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, StdSchedulerFactory.AUTO_GENERATE_INSTANCE_ID);
} else {
if (runtimeConfig.instanceId.equals(StdSchedulerFactory.SYSTEM_PROPERTY_AS_INSTANCE_ID)) {
LOGGER.warn("Prefer to configure the 'SystemPropertyInstanceIdGenerator' within the instance ID generators, "
+ "so the system property name can be changed and the application can be native.");
}
props.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, runtimeConfig.instanceId);
}
if (instanceIdGenerator != null) {
putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_SCHED_INSTANCE_ID_GENERATOR_PREFIX,
instanceIdGenerator);
}
putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_PLUGIN_PREFIX, buildTimeConfig.plugins);
putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_JOB_LISTENER_PREFIX, buildTimeConfig.jobListeners);
putExtensionConfigurationProperties(props, StdSchedulerFactory.PROP_TRIGGER_LISTENER_PREFIX,
buildTimeConfig.triggerListeners);
return props;
}
private void putExtensionConfigurationProperties(Properties props, String prefix,
Map configs) {
configs.forEach((configKey, config) -> {
putExtensionConfigurationProperties(props, String.format("%s.%s", prefix, configKey), config);
});
}
private void putExtensionConfigurationProperties(Properties props, String prefix, QuartzExtensionPointConfig config) {
props.put(String.format("%s.class", prefix), config.clazz);
config.properties.forEach((propName, propValue) -> {
props.put(String.format("%s.%s", prefix, propName), propValue);
});
}
@SuppressWarnings("deprecation")
StartMode initStartMode(SchedulerRuntimeConfig schedulerRuntimeConfig, QuartzRuntimeConfig quartzRuntimeConfig) {
if (schedulerRuntimeConfig.startMode.isPresent()) {
StartMode startMode = schedulerRuntimeConfig.startMode.get();
if (quartzRuntimeConfig.startMode.isPresent()) {
QuartzStartMode quartzStartMode = quartzRuntimeConfig.startMode.get();
if ((startMode == StartMode.NORMAL
&& quartzStartMode != QuartzStartMode.NORMAL)
|| (startMode == StartMode.FORCED && quartzStartMode != QuartzStartMode.FORCED)
|| (startMode == StartMode.HALTED && quartzStartMode != QuartzStartMode.HALTED)) {
throw new IllegalStateException(
"Inconsistent scheduler startup mode configuration; quarkus.scheduler.startMode=" + startMode
+ " does not match quarkus.quartz.startMode=" + quartzStartMode);
}
}
return startMode;
} else {
if (quartzRuntimeConfig.startMode.isPresent()) {
QuartzStartMode quartzStartMode = quartzRuntimeConfig.startMode.get();
switch (quartzStartMode) {
case NORMAL:
return StartMode.NORMAL;
case FORCED:
return StartMode.FORCED;
case HALTED:
return StartMode.HALTED;
default:
throw new IllegalStateException();
}
} else {
return StartMode.NORMAL;
}
}
}
private JobDetail createJobDetail(String identity, String invokerClassName) {
return JobBuilder.newJob(InvokerJob.class)
// new JobKey(identity, "io.quarkus.scheduler.Scheduler")
.withIdentity(identity, Scheduler.class.getName())
// this info is redundant but keep it for backward compatibility
.usingJobData(INVOKER_KEY, invokerClassName)
.requestRecovery().build();
}
private Optional> createTrigger(String identity, Scheduled scheduled, CronType cronType,
QuartzRuntimeConfig runtimeConfig, JobDetail jobDetail) {
ScheduleBuilder> scheduleBuilder;
String cron = SchedulerUtils.lookUpPropertyValue(scheduled.cron());
if (!cron.isEmpty()) {
if (SchedulerUtils.isOff(cron)) {
this.pause(identity);
return Optional.empty();
}
if (!CronType.QUARTZ.equals(cronType)) {
// Migrate the expression
Cron cronExpr = cronParser.parse(cron);
switch (cronType) {
case UNIX:
cron = CronMapper.fromUnixToQuartz().map(cronExpr).asString();
break;
case CRON4J:
cron = CronMapper.fromCron4jToQuartz().map(cronExpr).asString();
break;
default:
break;
}
}
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
ZoneId timeZone = SchedulerUtils.parseCronTimeZone(scheduled);
if (timeZone != null) {
cronScheduleBuilder.inTimeZone(TimeZone.getTimeZone(timeZone));
}
QuartzRuntimeConfig.QuartzMisfirePolicyConfig perJobConfig = runtimeConfig.misfirePolicyPerJobs
.getOrDefault(identity, runtimeConfig.cronTriggerConfig.misfirePolicyConfig);
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()) {
this.pause(identity);
return Optional.empty();
}
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(everyMillis.getAsLong())
.repeatForever();
QuartzRuntimeConfig.QuartzMisfirePolicyConfig perJobConfig = runtimeConfig.misfirePolicyPerJobs
.getOrDefault(identity, runtimeConfig.simpleTriggerConfig.misfirePolicyConfig);
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);
}
TriggerBuilder> triggerBuilder = TriggerBuilder.newTrigger()
.withIdentity(identity, Scheduler.class.getName())
.forJob(jobDetail)
.withSchedule(scheduleBuilder);
Long millisToAdd = null;
if (scheduled.delay() > 0) {
millisToAdd = scheduled.delayUnit().toMillis(scheduled.delay());
} else if (!scheduled.delayed().isEmpty()) {
millisToAdd = SchedulerUtils.parseDelayedAsMillis(scheduled);
}
if (millisToAdd != null) {
triggerBuilder.startAt(new Date(Instant.now()
.plusMillis(millisToAdd).toEpochMilli()));
}
return Optional.of(triggerBuilder);
}
class QuartzJobDefinition extends AbstractJobDefinition {
QuartzJobDefinition(String id) {
super(id);
}
@Override
public Trigger schedule() {
checkScheduled();
if (task == null && asyncTask == null) {
throw new IllegalStateException("Either sync or async task must be set");
}
scheduled = true;
ScheduledInvoker invoker;
if (task != null) {
// Use the default invoker to make sure the CDI request context is activated
invoker = new DefaultInvoker() {
@Override
public CompletionStage invokeBean(ScheduledExecution execution) {
try {
task.accept(execution);
return CompletableFuture.completedStage(null);
} catch (Exception e) {
return CompletableFuture.failedStage(e);
}
}
@Override
public boolean isRunningOnVirtualThread() {
return runOnVirtualThread;
}
};
} else {
invoker = new DefaultInvoker() {
@Override
public CompletionStage invokeBean(ScheduledExecution execution) {
try {
return asyncTask.apply(execution).subscribeAsCompletionStage();
} catch (Exception e) {
return CompletableFuture.failedStage(e);
}
}
@Override
public boolean isBlocking() {
return false;
}
};
}
Scheduled scheduled = new SyntheticScheduled(identity, cron, every, 0, TimeUnit.MINUTES, delayed,
overdueGracePeriod, concurrentExecution, skipPredicate, timeZone);
JobDetail jobDetail = createJobDetail(identity, QuartzSchedulerImpl.class.getName());
Optional> triggerBuilder = createTrigger(identity, scheduled, cronType, runtimeConfig, jobDetail);
if (triggerBuilder.isPresent()) {
invoker = SimpleScheduler.initInvoker(invoker, skippedExecutionEvent, successExecutionEvent,
failedExecutionEvent, concurrentExecution, skipPredicate);
org.quartz.Trigger trigger = triggerBuilder.get().build();
QuartzTrigger existing = scheduledTasks.putIfAbsent(identity, new QuartzTrigger(trigger.getKey(),
new Function<>() {
@Override
public org.quartz.Trigger apply(TriggerKey triggerKey) {
try {
return scheduler.getTrigger(triggerKey);
} catch (SchedulerException e) {
throw new IllegalStateException(e);
}
}
}, invoker,
SchedulerUtils.parseOverdueGracePeriod(scheduled, defaultOverdueGracePeriod),
runtimeConfig.runBlockingScheduledMethodOnQuartzThread, true, null));
if (existing != null) {
throw new IllegalStateException("A job with this identity is already scheduled: " + identity);
}
try {
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
throw new IllegalStateException(e);
}
}
return null;
}
}
/**
* Although this class is not part of the public API it must not be renamed in order to preserve backward compatibility. The
* name of this class can be stored in a Quartz table in the database. See https://github.com/quarkusio/quarkus/issues/29177
* for more information.
*/
static class InvokerJob implements Job {
final QuartzTrigger trigger;
final Vertx vertx;
InvokerJob(QuartzTrigger trigger, Vertx vertx) {
this.trigger = trigger;
this.vertx = vertx;
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
if (trigger != null && trigger.invoker != null) { // could be null from previous runs
if (trigger.invoker.isBlocking()) {
if (trigger.runBlockingMethodOnQuartzThread) {
try {
trigger.invoker.invoke(new QuartzScheduledExecution(trigger, jobExecutionContext));
} catch (Exception e) {
// already logged by the StatusEmitterInvoker
}
} else {
Context context = VertxContext.getOrCreateDuplicatedContext(vertx);
VertxContextSafetyToggle.setContextSafe(context, true);
if (trigger.invoker.isRunningOnVirtualThread()) {
// While counter-intuitive, we switch to a safe context, so that context is captured and attached
// to the virtual thread.
context.runOnContext(new Handler() {
@Override
public void handle(Void event) {
VirtualThreadsRecorder.getCurrent().execute(new Runnable() {
@Override
public void run() {
try {
trigger.invoker
.invoke(new QuartzScheduledExecution(trigger, jobExecutionContext));
} catch (Exception ignored) {
// already logged by the StatusEmitterInvoker
}
}
});
}
});
} else {
context.executeBlocking(new Callable
© 2015 - 2025 Weber Informatics LLC | Privacy Policy