View Javadoc

1   package com.atlassian.scheduler.quartz2;
2   
3   import com.atlassian.scheduler.SchedulerRuntimeException;
4   import com.atlassian.scheduler.SchedulerServiceException;
5   import com.atlassian.scheduler.config.JobId;
6   import com.atlassian.scheduler.config.JobRunnerKey;
7   import com.atlassian.scheduler.config.RunMode;
8   import com.atlassian.scheduler.core.AbstractSchedulerService;
9   import com.atlassian.scheduler.quartz2.spi.Quartz2SchedulerConfiguration;
10  import com.atlassian.util.concurrent.LazyReference;
11  import com.google.common.base.Supplier;
12  import com.google.common.base.Suppliers;
13  import org.quartz.JobBuilder;
14  import org.quartz.JobDetail;
15  import org.quartz.JobKey;
16  import org.quartz.Scheduler;
17  import org.quartz.SchedulerException;
18  import org.quartz.Trigger;
19  import org.quartz.TriggerBuilder;
20  import org.quartz.TriggerKey;
21  import org.quartz.impl.StdSchedulerFactory;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import javax.annotation.Nullable;
26  import java.util.Collection;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Properties;
30  import java.util.Set;
31  
32  import static com.atlassian.scheduler.config.RunMode.RUN_LOCALLY;
33  import static com.atlassian.scheduler.config.RunMode.RUN_ONCE_PER_CLUSTER;
34  import static com.atlassian.util.concurrent.Assertions.notNull;
35  import static org.quartz.JobKey.jobKey;
36  import static org.quartz.TriggerKey.triggerKey;
37  import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;
38  
39  /**
40   * Wraps the quartz scheduler to make it work a bit closer to how we need it to.
41   * Specifically, this hides Quartz's checked exceptions and provides the association
42   * between our jobId/jobRunnerKey and the trigger and group names.
43   */
44  class Quartz2SchedulerFacade {
45      private static final Logger LOG = LoggerFactory.getLogger(Quartz2SchedulerFacade.class);
46  
47      static final String QUARTZ_JOB_GROUP = "SchedulerServiceJobs";
48      static final String QUARTZ_TRIGGER_GROUP = "SchedulerServiceTriggers";
49      static final String QUARTZ_PARAMETERS_KEY = "parameters";
50  
51      private final Supplier<Scheduler> quartzRef;
52  
53      private Quartz2SchedulerFacade(final Supplier<Scheduler> quartzRef) {
54          this.quartzRef = quartzRef;
55      }
56  
57  
58      /**
59       * Creates the local scheduler facade by wrapping a supplied Scheduler instance.
60       */
61      static Quartz2SchedulerFacade createLocal(final AbstractSchedulerService schedulerService,
62                                                final Scheduler scheduler) throws SchedulerServiceException {
63          notNull("scheduler", scheduler);
64          return createFacade(schedulerService, scheduler, RUN_LOCALLY);
65      }
66  
67      /**
68       * Creates the clustered scheduler facade by wrapping a supplied Scheduler instance.
69       */
70      static Quartz2SchedulerFacade createClustered(final AbstractSchedulerService schedulerService,
71                                                    final Scheduler scheduler) throws SchedulerServiceException {
72          notNull("scheduler", scheduler);
73          return createFacade(schedulerService, scheduler, RUN_ONCE_PER_CLUSTER);
74      }
75  
76      /**
77       * Creates a schedule scheduler facade by wrapping a supplied scheduler instance.
78       */
79      private static Quartz2SchedulerFacade createFacade(final AbstractSchedulerService schedulerService,
80                                                         final Scheduler scheduler,
81                                                         final RunMode runMode) throws SchedulerServiceException {
82          try {
83              configureScheduler(scheduler, schedulerService, runMode);
84              final Supplier<Scheduler> quartzRef = Suppliers.ofInstance(scheduler);
85              return new Quartz2SchedulerFacade(quartzRef);
86          } catch (SchedulerException se) {
87              throw checked("Unable to configure the underlying Quartz scheduler", se);
88          }
89      }
90  
91  
92      /**
93       * Creates the local scheduler facade lazily using the supplied configuration.
94       */
95      static Quartz2SchedulerFacade createLocal(final AbstractSchedulerService schedulerService,
96                                                final Quartz2SchedulerConfiguration config) throws SchedulerServiceException {
97          notNull("config", config);
98          final Properties localSettings = notNull("config.getLocalSettings()", config.getLocalSettings());
99          return createFacade(schedulerService, localSettings, RUN_LOCALLY);
100     }
101 
102     /**
103      * Creates the clustered scheduler facade lazily using the supplied configuration.
104      */
105     static Quartz2SchedulerFacade createClustered(final AbstractSchedulerService schedulerService,
106                                                   final Quartz2SchedulerConfiguration config) throws SchedulerServiceException {
107         notNull("config", config);
108         final Properties clusteredSettings = notNull("config.getClusteredSettings()", config.getClusteredSettings());
109         return createFacade(schedulerService, clusteredSettings, RUN_ONCE_PER_CLUSTER);
110     }
111 
112     /**
113      * Creates a schedule scheduler facade lazily using the supplied configuration.
114      */
115     private static Quartz2SchedulerFacade createFacade(final AbstractSchedulerService schedulerService,
116                                                        final Properties customConfig, final RunMode runMode)
117             throws SchedulerServiceException {
118         try {
119             final Properties config = new Properties();
120             for (String key : customConfig.stringPropertyNames()) {
121                 config.setProperty(key, customConfig.getProperty(key));
122             }
123 
124             // JRA-23747 -- never allow Quartz to run its update check
125             config.setProperty("org.quartz.scheduler.skipUpdateCheck", "true");
126 
127             final StdSchedulerFactory schedulerFactory = new StdSchedulerFactory(config);
128             final Supplier<Scheduler> quartzRef = createQuartzRef(schedulerService, runMode, schedulerFactory);
129             return new Quartz2SchedulerFacade(quartzRef);
130         } catch (SchedulerException se) {
131             throw checked("Unable to create the underlying Quartz scheduler", se);
132         }
133     }
134 
135 
136     private static Supplier<Scheduler> createQuartzRef(final AbstractSchedulerService schedulerService,
137                                                        final RunMode runMode,
138                                                        final StdSchedulerFactory schedulerFactory) {
139         return new LazyReference<Scheduler>() {
140             @Override
141             protected Scheduler create() throws Exception {
142                 // SCHEDULER-11: Quartz cares about the thread's CCL.  This makes sure that
143                 // the class loader that was used to construct the Quartz1SchedulerService
144                 // is set as the thread's CCL at the time the scheduler is lazily constructed.
145                 // otherwise, if a plugin is given direct access to Quartz scheduler before
146                 // the application initializes it, the plugin bundle's classloader could be
147                 // set as the thread's CCL, with unpleasant results like HOT-6239.
148                 final Thread thd = Thread.currentThread();
149                 final ClassLoader originalContextClassLoader = thd.getContextClassLoader();
150                 try {
151                     thd.setContextClassLoader(schedulerService.getClass().getClassLoader());
152                     final Scheduler quartz = schedulerFactory.getScheduler();
153                     configureScheduler(quartz, schedulerService, runMode);
154                     return quartz;
155                 } finally {
156                     thd.setContextClassLoader(originalContextClassLoader);
157                 }
158             }
159         };
160     }
161 
162     static void configureScheduler(final Scheduler quartz,
163                                    final AbstractSchedulerService schedulerService,
164                                    final RunMode runMode) throws SchedulerException {
165         quartz.setJobFactory(new Quartz2JobFactory(schedulerService, runMode));
166     }
167 
168 
169     @Nullable
170     Trigger getTrigger(final JobId jobId) {
171         try {
172             return getScheduler().getTrigger(jobId(jobId));
173         } catch (SchedulerException se) {
174             logWarn("Error getting quartz trigger for '{}'", jobId, se);
175             return null;
176         }
177     }
178 
179     @Nullable
180     JobDetail getQuartzJob(final JobRunnerKey jobRunnerKey) {
181         try {
182             return getScheduler().getJobDetail(jobRunnerKey(jobRunnerKey));
183         } catch (SchedulerException se) {
184             logWarn("Error getting quartz job details for '{}'", jobRunnerKey, se);
185             return null;
186         }
187     }
188 
189     boolean hasAnyTriggers(final JobRunnerKey jobRunnerKey) {
190         return !getTriggersOfJob(jobRunnerKey).isEmpty();
191     }
192 
193     Collection<JobRunnerKey> getJobRunnerKeys() {
194         try {
195             final Set<JobKey> jobKeys = getScheduler().getJobKeys(jobGroupEquals(QUARTZ_JOB_GROUP));
196             final Set<JobRunnerKey> jobRunnerKeys = new HashSet<JobRunnerKey>((int) (jobKeys.size() * 1.25));
197             for (JobKey jobKey : jobKeys) {
198                 jobRunnerKeys.add(JobRunnerKey.of(jobKey.getName()));
199             }
200             return jobRunnerKeys;
201         } catch (SchedulerException se) {
202             throw unchecked("Could not get the triggers from Quartz", se);
203         }
204     }
205 
206     List<? extends Trigger> getTriggersOfJob(final JobRunnerKey jobRunnerKey) {
207         try {
208             return getScheduler().getTriggersOfJob(jobRunnerKey(jobRunnerKey));
209         } catch (SchedulerException se) {
210             throw unchecked("Could not get the triggers from Quartz", se);
211         }
212     }
213 
214     boolean deleteTrigger(final JobId jobId) {
215         try {
216             return getScheduler().unscheduleJob(jobId(jobId));
217         } catch (SchedulerException se) {
218             logWarn("Error removing Quartz trigger for '{}'", jobId, se);
219             return false;
220         }
221     }
222 
223     boolean deleteJob(final JobRunnerKey jobRunnerKey) {
224         try {
225             return getScheduler().deleteJob(jobRunnerKey(jobRunnerKey));
226         } catch (SchedulerException se) {
227             logWarn("Error removing Quartz job for '{}'", jobRunnerKey, se);
228             return false;
229         }
230     }
231 
232     private void scheduleJob(final TriggerBuilder<?> trigger) throws SchedulerServiceException {
233         try {
234             getScheduler().scheduleJob(trigger.build());
235         } catch (SchedulerException se) {
236             throw checked("Unable to create the Quartz trigger", se);
237         }
238     }
239 
240 
241     void scheduleJob(final JobRunnerKey jobRunnerKey, final TriggerBuilder<?> trigger) throws SchedulerServiceException {
242         if (getQuartzJob(jobRunnerKey) != null) {
243             scheduleJob(trigger.forJob(jobRunnerKey(jobRunnerKey)));
244             return;
245         }
246 
247         try {
248             final JobDetail quartzJob = JobBuilder.newJob()
249                     .withIdentity(jobRunnerKey(jobRunnerKey))
250                     .ofType(Quartz2Job.class)
251                     .storeDurably(false)
252                     .build();
253             getScheduler().scheduleJob(quartzJob, trigger.build());
254         } catch (SchedulerException se) {
255             throw checked("Unable to create the Quartz job and trigger", se);
256         }
257     }
258 
259     boolean unscheduleJob(final JobId jobId) {
260         final Trigger trigger = getTrigger(jobId);
261         if (trigger == null) {
262             return false;
263         }
264         final JobRunnerKey jobRunnerKey = JobRunnerKey.of(trigger.getJobKey().getName());
265         if (deleteTrigger(jobId) && !hasAnyTriggers(jobRunnerKey)) {
266             deleteJob(jobRunnerKey);
267         }
268         return true;
269     }
270 
271     void start() throws SchedulerServiceException {
272         try {
273             getScheduler().start();
274         } catch (SchedulerException se) {
275             throw checked("Quartz scheduler refused to start", se);
276         }
277     }
278 
279     void standby() throws SchedulerServiceException {
280         try {
281             getScheduler().standby();
282         } catch (SchedulerException se) {
283             throw checked("Quartz scheduler refused to enter standby mode", se);
284         }
285     }
286 
287     void shutdown() {
288         try {
289             getScheduler().shutdown();
290         } catch (SchedulerException se) {
291             LOG.error("Error shutting down internal scheduler", se);
292         }
293     }
294 
295     private Scheduler getScheduler() {
296         try {
297             return quartzRef.get();
298         } catch (LazyReference.InitializationException ex) {
299             throw unchecked("Error creating underlying Quartz scheduler", ex.getCause());
300         }
301     }
302 
303 
304     /**
305      * Log a warning message, including the full stack trace iff DEBUG logging is enabled.
306      *
307      * @param message the log message, which should have exactly one {@code {}} placeholder for {@code arg}
308      * @param arg     the argument for the message; typically a {@code JobId} or {@code JobRunnerKey}
309      * @param e       the exception, which will be full stack traced if DEBUG logging is enabled; otherwise, appended
310      *                to the message in {@code toString()} form.
311      */
312     private static void logWarn(String message, Object arg, Throwable e) {
313         if (LOG.isDebugEnabled()) {
314             LOG.warn(message, arg, e);
315         } else {
316             LOG.warn(message + ": {}", arg, e.toString());
317         }
318     }
319 
320     static TriggerKey jobId(JobId jobId) {
321         return triggerKey(jobId.toString(), QUARTZ_TRIGGER_GROUP);
322     }
323 
324     static JobKey jobRunnerKey(JobRunnerKey jobRunnerKey) {
325         return jobKey(jobRunnerKey.toString(), QUARTZ_JOB_GROUP);
326     }
327 
328     private static SchedulerServiceException checked(final String message, Throwable e) {
329         return new SchedulerServiceException(message, e);
330     }
331 
332     private static SchedulerRuntimeException unchecked(final String message, Throwable e) {
333         return new SchedulerRuntimeException(message, e);
334     }
335 }