package bo.gotthardt.queue; import bo.gotthardt.jersey.HK2Utils; import bo.gotthardt.schedule.HasScheduleConfigurations; import bo.gotthardt.schedule.ScheduleConfiguration; import bo.gotthardt.schedule.quartz.HasQuartzConfiguration; import bo.gotthardt.schedule.quartz.Quartz; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import io.dropwizard.Application; import io.dropwizard.Configuration; import io.dropwizard.cli.EnvironmentCommand; import io.dropwizard.lifecycle.Managed; import io.dropwizard.setup.Environment; import lombok.extern.slf4j.Slf4j; import net.sourceforge.argparse4j.inf.Namespace; import org.glassfish.hk2.api.ServiceLocator; import org.quartz.*; import org.quartz.impl.matchers.GroupMatcher; import org.quartz.utils.Key; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; /** * Dropwizard command for running message queue workers and scheduled jobs. * Message queue workers can be configured with which worker classes to instantiate, and how many threads of each. * Scheduled jobs can be configured with which job classes to instantiate, and a cron expression for when they should run. * * Make sure it has been set up for providing all the dependencies required for the workers and jobs being used. * * @author Bo Gotthardt */ @Slf4j public class WorkersCommand<T extends Configuration & HasWorkerConfigurations & HasScheduleConfigurations & HasQuartzConfiguration> extends EnvironmentCommand<T> implements Managed { private static final String GROUP_NAME = "cron"; private List<QueueWorker> workers = new ArrayList<>(); /** * Constructor. * @param application The application running this command. */ public WorkersCommand(Application<T> application) { super(application, "workers", "Runs message queue workers and scheduled jobs."); } @Override protected void run(Environment environment, Namespace namespace, T configuration) throws Exception { ServiceLocator locator = HK2Utils.getServiceLocator(environment); if (!configuration.getWorkers().isEmpty()) { setupWorkers(configuration.getWorkers(), environment, locator); log.info("Created {} workers.", workers.size()); } if (!configuration.getSchedules().isEmpty()) { Quartz quartz = new Quartz(configuration.getQuartz(), configuration.getDatabaseConfig(), locator); environment.lifecycle().manage(quartz); setupSchedules(configuration.getSchedules(), quartz.getScheduler()); } if (configuration.getWorkers().isEmpty() && configuration.getSchedules().isEmpty()) { log.error("Server started with no workers and no scheduled jobs configured. Is that on purpose?"); } } private void setupWorkers(List<WorkerConfiguration> configurations, Environment environment, ServiceLocator locator) { configurations.forEach(config -> { Class<? extends QueueWorker> workerClass = config.getWorker(); ExecutorService executorService = environment.lifecycle() .executorService(workerClass.getSimpleName() + "-%d") .maxThreads(config.getThreads()) .build(); for (int i = 0; i < config.getThreads(); i++) { QueueWorker<?> worker = locator.getService(workerClass); executorService.submit(worker); workers.add(worker); } log.info("Created {} thread{} for worker {}.", config.getThreads(), config.getThreads() > 1 ? "s" : "", workerClass.getSimpleName()); }); } private static void setupSchedules(List<ScheduleConfiguration> schedules, Scheduler scheduler) { Set<JobKey> configuredKeys = new HashSet<>(); schedules.forEach(config -> { Class<? extends Job> jobClass = config.getJob(); JobKey jobKey = new JobKey(config.getName(), GROUP_NAME); TriggerKey triggerKey = new TriggerKey(config.getName(), GROUP_NAME); configuredKeys.add(jobKey); JobDetail job = JobBuilder.newJob(jobClass) .withIdentity(jobKey) .storeDurably() .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity(triggerKey) .withSchedule(CronScheduleBuilder.cronSchedule(config.getCron())) .forJob(job) .build(); try { if (scheduler.checkExists(triggerKey)) { scheduler.rescheduleJob(triggerKey, trigger); } else { scheduler.addJob(job, true); scheduler.scheduleJob(trigger); } log.info("Scheduled job {} to run at '{}'", jobClass.getSimpleName(), config.getCron().getCronExpression()); } catch (SchedulerException e) { log.warn("Unable to schedule job {}", jobClass.getSimpleName(), e); } }); try { Set<JobKey> existingKeys = scheduler.getJobKeys(GroupMatcher.<JobKey>jobGroupEquals(GROUP_NAME)); Set<JobKey> removedKeys = Sets.difference(existingKeys, configuredKeys); scheduler.deleteJobs(Lists.newArrayList(removedKeys)); log.info("Jobs that are no longer configured and have been deleted: {}", removedKeys.stream().map(Key::getName).collect(Collectors.toList())); } catch (SchedulerException e) { // TODO throw new RuntimeException(e); } } @Override public void start() throws Exception { // Empty on purpose. } @Override public void stop() throws Exception { log.info("Canceling {} workers.", workers.size()); workers.forEach(QueueWorker::cancel); } }