View Javadoc

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