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)
52        throws SchedulerException {
53      JobDetail jobDetail =
54          JobBuilder.newJob(job)
55              .withIdentity(
56                  new JobKey(UUID.randomUUID().toString(), jobData.get(Task.TYPE_KEY).toString()))
57              .withDescription(jobData.getDescription())
58              .usingJobData(new JobDataMap(jobData))
59              .storeDurably(false)
60              .build();
61  
62      Trigger trigger =
63          TriggerBuilder.newTrigger()
64              .withIdentity(jobDetail.getKey().getName(), jobDetail.getKey().getGroup())
65              .startNow()
66              .withPriority(jobData.getPriority())
67              .usingJobData(
68                  SENTRY_SLUG_KEY, "monitor_slug_simple_trigger_" + jobData.get(Task.TYPE_KEY))
69              .forJob(jobDetail)
70              .build();
71  
72      scheduler.scheduleJob(jobDetail, Set.of(trigger), true);
73      return UUID.fromString(jobDetail.getKey().getName());
74    }
75  
76    /**
77     * Create a job and schedule it with a cron expression.
78     *
79     * @param job the task class to create
80     * @param jobData a map with job data, the {@code type} and {@code description} keys are mandatory
81     * @param cronExpression the cron expression
82     * @return the task name, a UUID
83     * @throws SchedulerException if the job could not be scheduled
84     */
85    public UUID createTask(
86        Class<? extends QuartzJobBean> job, TMJobDataMap jobData, String cronExpression)
87        throws SchedulerException {
88  
89      // Create a job
90      JobDetail jobDetail =
91          JobBuilder.newJob(job)
92              .withIdentity(
93                  new JobKey(UUID.randomUUID().toString(), jobData.get(Task.TYPE_KEY).toString()))
94              .withDescription(jobData.getDescription())
95              .usingJobData(new JobDataMap(jobData))
96              .build();
97  
98      // Create a trigger
99      Trigger trigger =
100         TriggerBuilder.newTrigger()
101             .withIdentity(jobDetail.getKey().getName(), jobDetail.getKey().getGroup())
102             .startAt(DateBuilder.futureDate(90, DateBuilder.IntervalUnit.SECOND))
103             .withPriority(jobData.getPriority())
104             .usingJobData(
105                 SENTRY_SLUG_KEY, "monitor_slug_cron_trigger_" + jobData.get(Task.TYPE_KEY))
106             .withSchedule(
107                 CronScheduleBuilder.cronSchedule(cronExpression)
108                     .withMisfireHandlingInstructionFireAndProceed())
109             .build();
110 
111     try {
112       scheduler.scheduleJob(jobDetail, trigger);
113     } catch (ObjectAlreadyExistsException ex) {
114       logger.warn(
115           "Job {} with trigger {} has not bean added to scheduler as it already exists.",
116           jobDetail.getKey(),
117           trigger.getKey());
118       return null;
119     }
120 
121     return UUID.fromString(jobDetail.getKey().getName());
122   }
123 
124   /**
125    * Reschedule a task using updated job data.
126    *
127    * @param jobKey the job key
128    * @param newJobData the new job data
129    * @throws SchedulerException if the job could not be rescheduled
130    */
131   public void updateTask(JobKey jobKey, TMJobDataMap newJobData) throws SchedulerException {
132 
133     if (scheduler.checkExists(jobKey)) {
134       // there should only ever be one trigger for a job in TM
135       Trigger oldTrigger = scheduler.getTriggersOfJob(jobKey).get(0);
136 
137       if (scheduler.checkExists(oldTrigger.getKey())) {
138 
139         JobDetail jobDetail = scheduler.getJobDetail(jobKey);
140         JobDataMap jobDataMap = jobDetail.getJobDataMap();
141         jobDataMap.putAll(newJobData);
142 
143         Trigger newTrigger =
144             TriggerBuilder.newTrigger()
145                 .withIdentity(jobKey.getName(), jobKey.getGroup())
146                 .startAt(DateBuilder.futureDate(90, DateBuilder.IntervalUnit.SECOND))
147                 .withPriority(jobDataMap.getInt(Task.PRIORITY_KEY))
148                 .usingJobData(
149                     SENTRY_SLUG_KEY,
150                     "monitor_slug_cron_trigger_" + jobDataMap.get(Task.TYPE_KEY).toString())
151                 .withSchedule(
152                     CronScheduleBuilder.cronSchedule(jobDataMap.getString(Task.CRON_EXPRESSION_KEY))
153                         .withMisfireHandlingInstructionFireAndProceed())
154                 .build();
155 
156         scheduler.addJob(jobDetail, true, true);
157         scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger);
158       }
159     }
160   }
161 
162   /**
163    * Get the job key for a given type and uuid.
164    *
165    * @param jobType the type of the job
166    * @param uuid the uuid of the job
167    * @return the job key
168    * @throws SchedulerException when the scheduler cannot be reached
169    */
170   @Nullable
171   public JobKey getJobKey(TaskType jobType, UUID uuid) throws SchedulerException {
172     logger.debug("Finding job key for task {}:{}", jobType, uuid);
173     return scheduler.getJobKeys(GroupMatcher.groupEquals(jobType.getValue())).stream()
174         .filter(jobkey -> jobkey.getName().equals(uuid.toString()))
175         .findFirst()
176         .orElse(null);
177   }
178 }