All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.jooby.quartz.QuartzModule Maven / Gradle / Ivy

There is a newer version: 3.5.3
Show newest version
/*
 * Jooby https://jooby.io
 * Apache License Version 2.0 https://jooby.io/LICENSE.txt
 * Copyright 2014 Edgar Espina
 */
package io.jooby.quartz;

import static io.jooby.SneakyThrows.throwingFunction;
import static org.quartz.impl.StdSchedulerFactory.PROP_SCHED_INSTANCE_ID;
import static org.quartz.impl.matchers.GroupMatcher.groupEquals;

import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.sql.DataSource;

import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.jdbcjobstore.JobStoreTX;
import org.quartz.simpl.PropertySettingJobFactory;
import org.quartz.utils.DBConnectionManager;
import org.slf4j.Logger;

import com.typesafe.config.Config;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.ServiceKey;
import io.jooby.ServiceRegistry;
import io.jooby.SneakyThrows;
import io.jooby.internal.quartz.ConnectionProviderImpl;
import io.jooby.internal.quartz.JobDelegate;
import io.jooby.internal.quartz.JobFactoryImpl;
import io.jooby.internal.quartz.JobGenerator;

/**
 * Scheduler module using Quartz: http://www.quartz-scheduler.org.
 *
 * 
{@code
 * {
 *    install(new QuartzModule(SampleJob.class));
 *
 * }
 *
 * public class SampleJob implements Job {
 *
 *   #64;Scheduled("10m")
 *   public void execute(JobExecutionContext context) {
 *
 *   }
 * }
 * }
* * Implementation of {@link org.quartz.Job} is optional: * *
{@code
 * public class MyJobs {
 *
 *   #64;Scheduled("1m")
 *   public void everyMinute() {
 *     ....
 *   }
 *
 *   #64;Scheduled("1h")
 *  *   public void everyHour() {
 *  *     ....
 *  *   }
 * }
 * }
* * Cron expression are supported too. Check the {@link Scheduled} annotation for possible schedule * expressions. * *

Job key are generated from container class and method name. Example: SampleJob.execute, * MyJob.everyMinute, MyJob.everyHour. * *

Jobs can be enabled/disabled (paused) at start up time by setting the enabled * property for each job key: * *

 *   org.quartz.jobs.SampleJob.execute.enabled = false
 * 
* * Now the SampleJob.execute is going to be paused at startup time. * *

The {@link QuartzApp} added a REST API to trigger, interrupt, pause, resume jobs. * * @author edgar * @since 2.5.1 */ public class QuartzModule implements Extension { private List> jobs; private Scheduler scheduler; private Boolean cleanStaleJobs; private boolean cleanJobs; /** * Creates Quartz module and register the given jobs. * * @param jobs Job classes. */ public QuartzModule(final Class... jobs) { this.jobs = Arrays.asList(jobs); } /** * Creates Quartz module and register the given jobs. * * @param jobs Job classes. */ public QuartzModule(final List> jobs) { this.jobs = jobs; } /** * Creates Quartz module and register the given jobs. Uses an user provided schedule, schedule is * started at application start up time and shutdown on application shutdown. * * @param scheduler Provided scheduler. * @param jobs Job classes. */ public QuartzModule(@NonNull Scheduler scheduler, final Class... jobs) { this.scheduler = scheduler; this.jobs = Arrays.asList(jobs); } /** * Creates Quartz module and register the given jobs. Uses an user provided schedule, schedule is * started at application start up time and shutdown on application shutdown. * * @param scheduler Provided scheduler. * @param jobs Job classes. */ public QuartzModule(@NonNull Scheduler scheduler, final List> jobs) { this.scheduler = scheduler; this.jobs = jobs; } /** * Lookup for existing (persisted) jobs and compare with the job list from {@link * #QuartzModule(Class[])}. Delete any persisted job that is not in the list. * * @param cleanStaleJobs True to clear/delete stale job and triggers. * @return This module. */ public QuartzModule cleanStaleJobs(boolean cleanStaleJobs) { this.cleanStaleJobs = cleanStaleJobs; return this; } /** * Safely clear/delete all jobs before schedule startup. Use with caution. * * @param cleanJobs True to clear/delete all jobs and triggers. * @return This module. */ public QuartzModule cleanJobs(boolean cleanJobs) { this.cleanJobs = cleanJobs; return this; } @Override public void install(@NonNull Jooby application) throws Exception { Config config = application.getConfig(); Map jobMap = JobGenerator.build(application, jobs); Properties properties = properties(config); this.cleanStaleJobs = computeCleanStaleJobs(this.scheduler == null); Scheduler scheduler = this.scheduler == null ? newScheduler(application) : this.scheduler; var context = scheduler.getContext(); context.put("registry", application); ServiceRegistry services = application.getServices(); services.putIfAbsent(Scheduler.class, scheduler); String schedulerName = scheduler.getSchedulerName(); if (services.put(ServiceKey.key(Scheduler.class, schedulerName), scheduler) != null) { throw new IllegalStateException("Scheduler already exists: " + schedulerName); } application.onStarted( () -> { if (scheduler.isStarted()) { scheduler.standby(); } if (this.cleanJobs) { // clear all jobs scheduler.clear(); } else { if (this.cleanStaleJobs) { cleanStaleJobs(application.getLog(), scheduler, jobs); } } for (Map.Entry e : jobMap.entrySet()) { JobDetail jobDetail = e.getKey(); Trigger trigger = e.getValue(); var jobClass = jobDetail.getJobClass(); if (JobDelegate.class.isAssignableFrom(jobClass)) { var jobMethod = (Method) jobDetail.getJobDataMap().remove("jobMethod"); context.put(jobDetail.getKey().toString(), jobMethod); } boolean jobEnabled = isJobPaused(properties, jobDetail.getKey()); if (scheduler.checkExists(jobDetail.getKey())) { // make sure trigger is updated scheduler.rescheduleJob(trigger.getKey(), trigger); } else { scheduler.scheduleJob(jobDetail, trigger); } if (jobEnabled) { application.getLog().info("{} {}", jobDetail.getKey(), trigger.getDescription()); } else { scheduler.pauseJob(jobDetail.getKey()); application .getLog() .info("{} {} (PAUSED)", jobDetail.getKey(), trigger.getDescription()); } } if (!scheduler.isStarted()) { scheduler.start(); } }); boolean waitForJobsToComplete = Boolean.parseBoolean(properties.getProperty("org.quartz.scheduler.waitForJobsToComplete")); application.onStop(() -> scheduler.shutdown(waitForJobsToComplete)); } private boolean computeCleanStaleJobs(boolean defaults) { return this.cleanStaleJobs == null ? defaults : this.cleanStaleJobs.booleanValue(); } /** * Cleanup any job that was persisted in previous execution and was removed from job list. * * @param log * @param scheduler * @param jobs * @throws SchedulerException */ private static void cleanStaleJobs(Logger log, Scheduler scheduler, List> jobs) throws SchedulerException { var savedKeys = scheduler.getJobGroupNames().stream() .flatMap(throwingFunction(group -> scheduler.getJobKeys(groupEquals(group)).stream())) .collect(Collectors.toSet()); var activeKeys = jobs.stream() .flatMap(job -> JobGenerator.jobMethod(job).stream()) .map(it -> new JobKey(it.getName(), it.getDeclaringClass().getSimpleName())) .collect(Collectors.toSet()); savedKeys.removeAll(activeKeys); if (!savedKeys.isEmpty()) { log.debug("removing stale job(s): {}", savedKeys); for (JobKey key : savedKeys) { try { var deleted = scheduler.deleteJob(key); log.debug("job {} deleted: {}", key, deleted); } catch (Exception cause) { log.debug("unable to delete job: {}", key, cause); } } } } /** * Creates a new scheduler. * * @param application Application. * @return New scheduler. */ public static @NonNull Scheduler newScheduler(@NonNull Jooby application) { try { Scheduler scheduler = newScheduleFactory(application).getScheduler(); scheduler.setJobFactory(new JobFactoryImpl(application, new PropertySettingJobFactory())); return scheduler; } catch (SchedulerException e) { throw SneakyThrows.propagate(e); } } /** * Creates a new scheduler factory. * * @param application Application. * @return New scheduler factory. */ public static @NonNull StdSchedulerFactory newScheduleFactory(@NonNull Jooby application) { try { Properties properties = properties(application.getConfig()); if (JobStoreTX.class.getName().equals(properties.getProperty("org.quartz.jobStore.class"))) { configureJdbcStore(application, properties); } return new StdSchedulerFactory(properties); } catch (SchedulerException e) { throw SneakyThrows.propagate(e); } } private static boolean isJobPaused(Properties properties, JobKey key) { return Stream.of( "org.quartz.jobs." + key.toString() + ".enabled", "org.quartz.jobs." + key.getGroup() + ".enabled") .map(properties::getProperty) .filter(Objects::nonNull) .findFirst() .map(v -> v.equals("true")) .orElse(true); } private static Properties properties(final Config config) { Properties props = new Properties(); props.setProperty("org.quartz.scheduler.waitForJobsToComplete", "false"); hostName().ifPresent(hostname -> props.setProperty(PROP_SCHED_INSTANCE_ID, hostname)); props.setProperty("org.quartz.scheduler.skipUpdateCheck", "true"); props.setProperty("org.quartz.threadPool.threadNamePrefix", "scheduler"); props.setProperty( "org.quartz.threadPool.threadCount", Integer.toString(Runtime.getRuntime().availableProcessors())); if (config.hasPath("org.quartz")) { // dump config .getConfig("org.quartz") .entrySet() .forEach( e -> props.setProperty( "org.quartz." + e.getKey(), e.getValue().unwrapped().toString())); } return props; } private static Optional hostName() { try { return Optional.ofNullable(InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { return Optional.empty(); } } private static void configureJdbcStore(Jooby application, Properties properties) { String dataSourceName = properties.getProperty("org.quartz.jobStore.dataSource"); ServiceRegistry registry = application.getServices(); DataSource dataSource = Optional.ofNullable(dataSourceName) .map(key -> registry.getOrNull(ServiceKey.key(DataSource.class, key))) .orElseGet(() -> registry.getOrNull(DataSource.class)); if (dataSource == null) { // TODO: replace with usage exception throw new IllegalArgumentException("DataSource not found: " + dataSourceName); } String dataSourceKey = Optional.ofNullable(dataSourceName).orElse("db"); properties.setProperty("org.quartz.jobStore.dataSource", dataSourceKey); DBConnectionManager.getInstance() .addConnectionProvider(dataSourceKey, new ConnectionProviderImpl(dataSource)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy