View Javadoc
1   /*
2    * Copyright (C) 2024 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.scheduling;
7   
8   import java.lang.invoke.MethodHandles;
9   import java.util.Set;
10  import java.util.UUID;
11  import org.jspecify.annotations.Nullable;
12  import org.quartz.CronScheduleBuilder;
13  import org.quartz.DateBuilder;
14  import org.quartz.JobBuilder;
15  import org.quartz.JobDataMap;
16  import org.quartz.JobDetail;
17  import org.quartz.JobKey;
18  import org.quartz.ObjectAlreadyExistsException;
19  import org.quartz.Scheduler;
20  import org.quartz.SchedulerException;
21  import org.quartz.Trigger;
22  import org.quartz.TriggerBuilder;
23  import org.quartz.impl.matchers.GroupMatcher;
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  import org.springframework.beans.factory.annotation.Autowired;
27  import org.springframework.scheduling.quartz.QuartzJobBean;
28  import org.springframework.stereotype.Service;
29  
30  @Service
31  public class TaskManagerService {
32    private static final Logger logger =
33        LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
34  
35    private final Scheduler scheduler;
36  
37    public TaskManagerService(@Autowired Scheduler scheduler) {
38      this.scheduler = scheduler;
39    }
40  
41    /**
42     * Create a one-time job and schedule it to start immediately.
43     *
44     * @param job the task class to create
45     * @param jobData a map with job data, the {@code type} and {@code description} keys are mandatory
46     * @return the task name, a hash of the description
47     * @throws SchedulerException if the job could not be scheduled
48     */
49    public UUID createTask(Class<? extends QuartzJobBean> job, TMJobDataMap jobData) throws SchedulerException {
50      JobDetail jobDetail = JobBuilder.newJob(job)
51          .withIdentity(new JobKey(
52              UUID.randomUUID().toString(), jobData.get(Task.TYPE_KEY).toString()))
53          .withDescription(jobData.getDescription())
54          .usingJobData(new JobDataMap(jobData))
55          .storeDurably(false)
56          .build();
57  
58      Trigger trigger = TriggerBuilder.newTrigger()
59          .withIdentity(jobDetail.getKey().getName(), jobDetail.getKey().getGroup())
60          .startNow()
61          .withPriority(jobData.getPriority())
62          .forJob(jobDetail)
63          .build();
64  
65      scheduler.scheduleJob(jobDetail, Set.of(trigger), true);
66      return UUID.fromString(jobDetail.getKey().getName());
67    }
68  
69    /**
70     * Create a job and schedule it with a cron expression.
71     *
72     * @param job the task class to create
73     * @param jobData a map with job data, the {@code type} and {@code description} keys are mandatory
74     * @param cronExpression the cron expression
75     * @return the task name, a UUID
76     * @throws SchedulerException if the job could not be scheduled
77     */
78    public UUID createTask(Class<? extends QuartzJobBean> job, TMJobDataMap jobData, String cronExpression)
79        throws SchedulerException {
80  
81      // Create a job
82      JobDetail jobDetail = JobBuilder.newJob(job)
83          .withIdentity(new JobKey(
84              UUID.randomUUID().toString(), jobData.get(Task.TYPE_KEY).toString()))
85          .withDescription(jobData.getDescription())
86          .usingJobData(new JobDataMap(jobData))
87          .build();
88  
89      // Create a trigger
90      Trigger trigger = TriggerBuilder.newTrigger()
91          .withIdentity(jobDetail.getKey().getName(), jobDetail.getKey().getGroup())
92          .startAt(DateBuilder.futureDate(90, DateBuilder.IntervalUnit.SECOND))
93          .withPriority(jobData.getPriority())
94          .withSchedule(
95              CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionFireAndProceed())
96          .build();
97  
98      try {
99        scheduler.scheduleJob(jobDetail, trigger);
100     } catch (ObjectAlreadyExistsException ex) {
101       logger.warn(
102           "Job {} with trigger {} has not bean added to scheduler as it already exists.",
103           jobDetail.getKey(),
104           trigger.getKey());
105       return null;
106     }
107 
108     return UUID.fromString(jobDetail.getKey().getName());
109   }
110 
111   /**
112    * Reschedule a task using updated job data.
113    *
114    * @param jobKey the job key
115    * @param newJobData the new job data
116    * @throws SchedulerException if the job could not be rescheduled
117    */
118   public void updateTask(JobKey jobKey, TMJobDataMap newJobData) throws SchedulerException {
119 
120     if (scheduler.checkExists(jobKey)) {
121       // there should only ever be one trigger for a job in TM
122       Trigger oldTrigger = scheduler.getTriggersOfJob(jobKey).getFirst();
123 
124       if (scheduler.checkExists(oldTrigger.getKey())) {
125 
126         JobDetail jobDetail = scheduler.getJobDetail(jobKey);
127         JobDataMap jobDataMap = jobDetail.getJobDataMap();
128         jobDataMap.putAll(newJobData);
129 
130         Trigger newTrigger = TriggerBuilder.newTrigger()
131             .withIdentity(jobKey.getName(), jobKey.getGroup())
132             .startAt(DateBuilder.futureDate(90, DateBuilder.IntervalUnit.SECOND))
133             .withPriority(jobDataMap.getInt(Task.PRIORITY_KEY))
134             .withSchedule(CronScheduleBuilder.cronSchedule(jobDataMap.getString(Task.CRON_EXPRESSION_KEY))
135                 .withMisfireHandlingInstructionFireAndProceed())
136             .build();
137 
138         scheduler.addJob(jobDetail, true, true);
139         scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger);
140       }
141     }
142   }
143 
144   /**
145    * Get the job key for a given type and uuid.
146    *
147    * @param jobType the type of the job
148    * @param uuid the uuid of the job
149    * @return the job key
150    * @throws SchedulerException when the scheduler cannot be reached
151    */
152   @Nullable public JobKey getJobKey(TaskType jobType, UUID uuid) throws SchedulerException {
153     logger.debug("Finding job key for task {}:{}", jobType, uuid);
154     return scheduler.getJobKeys(GroupMatcher.groupEquals(jobType.getValue())).stream()
155         .filter(jobkey -> jobkey.getName().equals(uuid.toString()))
156         .findFirst()
157         .orElse(null);
158   }
159 
160   /**
161    * Delete all tasks in a specific group.
162    *
163    * @param groupName the name of the group to delete tasks from
164    * @throws SchedulerException if the scheduler cannot be reached or if there is an error deleting the tasks
165    */
166   public void deleteTasksByGroupName(String groupName) throws SchedulerException {
167     logger.debug("Deleting tasks in group: {}", groupName);
168     Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.groupEquals(groupName));
169     for (JobKey jobKey : jobKeys) {
170       logger.info("Deleting task: {}", jobKey);
171       scheduler.deleteJob(jobKey);
172     }
173   }
174 }