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