View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase;
20  
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.LinkedHashMap;
24  import java.util.Map.Entry;
25  import java.util.concurrent.ScheduledFuture;
26  import java.util.concurrent.ScheduledThreadPoolExecutor;
27  import java.util.concurrent.ThreadFactory;
28  import java.util.concurrent.atomic.AtomicInteger;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.hadoop.hbase.ScheduledChore.ChoreServicer;
33  import org.apache.hadoop.hbase.classification.InterfaceAudience;
34  import org.apache.hadoop.hbase.classification.InterfaceStability;
35  
36  /**
37   * ChoreService is a service that can be used to schedule instances of {@link ScheduledChore} to run
38   * periodically while sharing threads. The ChoreService is backed by a
39   * {@link ScheduledThreadPoolExecutor} whose core pool size changes dynamically depending on the
40   * number of {@link ScheduledChore} scheduled. All of the threads in the core thread pool of the
41   * underlying {@link ScheduledThreadPoolExecutor} are set to be daemon threads.
42   * <p>
43   * The ChoreService provides the ability to schedule, cancel, and trigger instances of
44   * {@link ScheduledChore}. The ChoreService also provides the ability to check on the status of
45   * scheduled chores. The number of threads used by the ChoreService changes based on the scheduling
46   * load and whether or not the scheduled chores are executing on time. As more chores are scheduled,
47   * there may be a need to increase the number of threads if it is noticed that chores are no longer
48   * meeting their scheduled start times. On the other hand, as chores are cancelled, an attempt is
49   * made to reduce the number of running threads to see if chores can still meet their start times
50   * with a smaller thread pool.
51   * <p>
52   * When finished with a ChoreService it is good practice to call {@link ChoreService#shutdown()}.
53   * Calling this method ensures that all scheduled chores are cancelled and cleaned up properly.
54   */
55  @InterfaceAudience.Public
56  @InterfaceStability.Stable
57  public class ChoreService implements ChoreServicer {
58    private static final Log LOG = LogFactory.getLog(ChoreService.class);
59  
60    /**
61     * The minimum number of threads in the core pool of the underlying ScheduledThreadPoolExecutor
62     */
63    @InterfaceAudience.Private
64    public final static int MIN_CORE_POOL_SIZE = 1;
65  
66    /**
67     * This thread pool is used to schedule all of the Chores
68     */
69    private final ScheduledThreadPoolExecutor scheduler;
70  
71    /**
72     * Maps chores to their futures. Futures are used to control a chore's schedule
73     */
74    private final HashMap<ScheduledChore, ScheduledFuture<?>> scheduledChores;
75  
76    /**
77     * Maps chores to Booleans which indicate whether or not a chore has caused an increase in the
78     * core pool size of the ScheduledThreadPoolExecutor. Each chore should only be allowed to
79     * increase the core pool size by 1 (otherwise a single long running chore whose execution is
80     * longer than its period would be able to spawn too many threads).
81     */
82    private final HashMap<ScheduledChore, Boolean> choresMissingStartTime;
83  
84    /**
85     * The coreThreadPoolPrefix is the prefix that will be applied to all threads within the
86     * ScheduledThreadPoolExecutor. The prefix is typically related to the Server that the service is
87     * running on. The prefix is useful because it allows us to monitor how the thread pool of a
88     * particular service changes over time VIA thread dumps.
89     */
90    private final String coreThreadPoolPrefix;
91  
92    /**
93     *
94     * @param coreThreadPoolPrefix Prefix that will be applied to the Thread name of all threads
95     *          spawned by this service
96     */
97    @InterfaceAudience.Private
98    public ChoreService(final String coreThreadPoolPrefix) {
99      this(coreThreadPoolPrefix, MIN_CORE_POOL_SIZE, false);
100   }
101 
102   /**
103    * @param coreThreadPoolPrefix Prefix that will be applied to the Thread name of all threads
104    *          spawned by this service
105    * @param jitter Should chore service add some jitter for all of the scheduled chores. When set
106    *               to true this will add -10% to 10% jitter.
107    */
108   public ChoreService(final String coreThreadPoolPrefix, final boolean jitter) {
109     this(coreThreadPoolPrefix, MIN_CORE_POOL_SIZE, jitter);
110   }
111 
112   /**
113    * @param coreThreadPoolPrefix Prefix that will be applied to the Thread name of all threads
114    *          spawned by this service
115    * @param corePoolSize The initial size to set the core pool of the ScheduledThreadPoolExecutor 
116    *          to during initialization. The default size is 1, but specifying a larger size may be
117    *          beneficial if you know that 1 thread will not be enough.
118    * @param jitter Should chore service add some jitter for all of the scheduled chores. When set
119    *               to true this will add -10% to 10% jitter.
120    */
121   public ChoreService(final String coreThreadPoolPrefix, int corePoolSize, boolean jitter) {
122     this.coreThreadPoolPrefix = coreThreadPoolPrefix;
123     if (corePoolSize < MIN_CORE_POOL_SIZE)  {
124       corePoolSize = MIN_CORE_POOL_SIZE;
125     }
126 
127     final ThreadFactory threadFactory = new ChoreServiceThreadFactory(coreThreadPoolPrefix);
128     if (jitter) {
129       scheduler = new JitterScheduledThreadPoolExecutorImpl(corePoolSize, threadFactory, 0.1);
130     } else {
131       scheduler = new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
132     }
133 
134     scheduler.setRemoveOnCancelPolicy(true);
135     scheduledChores = new HashMap<ScheduledChore, ScheduledFuture<?>>();
136     choresMissingStartTime = new HashMap<ScheduledChore, Boolean>();
137   }
138 
139   /**
140    * @param chore Chore to be scheduled. If the chore is already scheduled with another ChoreService
141    *          instance, that schedule will be cancelled (i.e. a Chore can only ever be scheduled
142    *          with a single ChoreService instance).
143    * @return true when the chore was successfully scheduled. false when the scheduling failed
144    *         (typically occurs when a chore is scheduled during shutdown of service)
145    */
146   public synchronized boolean scheduleChore(ScheduledChore chore) {
147     if (chore == null) {
148       return false;
149     }
150 
151     try {
152       if (chore.getPeriod() <= 0) {
153         LOG.info("Chore " + chore + " is disabled because its period is not positive.");
154         return false;
155       }
156       LOG.info("Chore " + chore + " is enabled.");
157       chore.setChoreServicer(this);
158       ScheduledFuture<?> future =
159           scheduler.scheduleAtFixedRate(chore, chore.getInitialDelay(), chore.getPeriod(),
160             chore.getTimeUnit());
161       scheduledChores.put(chore, future);
162       return true;
163     } catch (Exception exception) {
164       if (LOG.isInfoEnabled()) {
165         LOG.info("Could not successfully schedule chore: " + chore.getName());
166       }
167       return false;
168     }
169   }
170 
171   /**
172    * @param chore The Chore to be rescheduled. If the chore is not scheduled with this ChoreService
173    *          yet then this call is equivalent to a call to scheduleChore.
174    */
175   private synchronized void rescheduleChore(ScheduledChore chore) {
176     if (chore == null) return;
177 
178     if (scheduledChores.containsKey(chore)) {
179       ScheduledFuture<?> future = scheduledChores.get(chore);
180       future.cancel(false);
181     }
182     scheduleChore(chore);
183   }
184 
185   @InterfaceAudience.Private
186   @Override
187   public synchronized void cancelChore(ScheduledChore chore) {
188     cancelChore(chore, true);
189   }
190 
191   @InterfaceAudience.Private
192   @Override
193   public synchronized void cancelChore(ScheduledChore chore, boolean mayInterruptIfRunning) {
194     if (chore != null && scheduledChores.containsKey(chore)) {
195       ScheduledFuture<?> future = scheduledChores.get(chore);
196       future.cancel(mayInterruptIfRunning);
197       scheduledChores.remove(chore);
198 
199       // Removing a chore that was missing its start time means it may be possible
200       // to reduce the number of threads
201       if (choresMissingStartTime.containsKey(chore)) {
202         choresMissingStartTime.remove(chore);
203         requestCorePoolDecrease();
204       }
205     }
206   }
207 
208   @InterfaceAudience.Private
209   @Override
210   public synchronized boolean isChoreScheduled(ScheduledChore chore) {
211     return chore != null && scheduledChores.containsKey(chore)
212         && !scheduledChores.get(chore).isDone();
213   }
214 
215   @InterfaceAudience.Private
216   @Override
217   public synchronized boolean triggerNow(ScheduledChore chore) {
218     if (chore == null) {
219       return false;
220     } else {
221       rescheduleChore(chore);
222       return true;
223     }
224   }
225 
226   /**
227    * @return number of chores that this service currently has scheduled
228    */
229   int getNumberOfScheduledChores() {
230     return scheduledChores.size();
231   }
232 
233   /**
234    * @return number of chores that this service currently has scheduled that are missing their
235    *         scheduled start time
236    */
237   int getNumberOfChoresMissingStartTime() {
238     return choresMissingStartTime.size();
239   }
240 
241   /**
242    * @return number of threads in the core pool of the underlying ScheduledThreadPoolExecutor
243    */
244   int getCorePoolSize() {
245     return scheduler.getCorePoolSize();
246   }
247 
248   /**
249    * Custom ThreadFactory used with the ScheduledThreadPoolExecutor so that all the threads are
250    * daemon threads, and thus, don't prevent the JVM from shutting down
251    */
252   static class ChoreServiceThreadFactory implements ThreadFactory {
253     private final String threadPrefix;
254     private final static String THREAD_NAME_SUFFIX = "_ChoreService_";
255     private AtomicInteger threadNumber = new AtomicInteger(1);
256 
257     /**
258      * @param threadPrefix The prefix given to all threads created by this factory
259      */
260     public ChoreServiceThreadFactory(final String threadPrefix) {
261       this.threadPrefix = threadPrefix;
262     }
263 
264     @Override
265     public Thread newThread(Runnable r) {
266       Thread thread =
267           new Thread(r, threadPrefix + THREAD_NAME_SUFFIX + threadNumber.getAndIncrement());
268       thread.setDaemon(true);
269       return thread;
270     }
271   }
272 
273   /**
274    * Represents a request to increase the number of core pool threads. Typically a request
275    * originates from the fact that the current core pool size is not sufficient to service all of
276    * the currently running Chores
277    * @return true when the request to increase the core pool size succeeds
278    */
279   private synchronized boolean requestCorePoolIncrease() {
280     // There is no point in creating more threads than scheduledChores.size since scheduled runs
281     // of the same chore cannot run concurrently (i.e. happen-before behavior is enforced
282     // amongst occurrences of the same chore).
283     if (scheduler.getCorePoolSize() < scheduledChores.size()) {
284       scheduler.setCorePoolSize(scheduler.getCorePoolSize() + 1);
285       printChoreServiceDetails("requestCorePoolIncrease");
286       return true;
287     }
288     return false;
289   }
290 
291   /**
292    * Represents a request to decrease the number of core pool threads. Typically a request
293    * originates from the fact that the current core pool size is more than sufficient to service the
294    * running Chores.
295    */
296   private synchronized void requestCorePoolDecrease() {
297     if (scheduler.getCorePoolSize() > MIN_CORE_POOL_SIZE) {
298       scheduler.setCorePoolSize(scheduler.getCorePoolSize() - 1);
299       printChoreServiceDetails("requestCorePoolDecrease");
300     }
301   }
302 
303   @InterfaceAudience.Private
304   @Override
305   public synchronized void onChoreMissedStartTime(ScheduledChore chore) {
306     if (chore == null || !scheduledChores.containsKey(chore)) return;
307 
308     // If the chore has not caused an increase in the size of the core thread pool then request an
309     // increase. This allows each chore missing its start time to increase the core pool size by
310     // at most 1.
311     if (!choresMissingStartTime.containsKey(chore) || !choresMissingStartTime.get(chore)) {
312       choresMissingStartTime.put(chore, requestCorePoolIncrease());
313     }
314 
315     // Must reschedule the chore to prevent unnecessary delays of chores in the scheduler. If
316     // the chore is NOT rescheduled, future executions of this chore will be delayed more and
317     // more on each iteration. This hurts us because the ScheduledThreadPoolExecutor allocates
318     // idle threads to chores based on how delayed they are.
319     rescheduleChore(chore);
320     printChoreDetails("onChoreMissedStartTime", chore);
321   }
322 
323   /**
324    * shutdown the service. Any chores that are scheduled for execution will be cancelled. Any chores
325    * in the middle of execution will be interrupted and shutdown. This service will be unusable
326    * after this method has been called (i.e. future scheduling attempts will fail).
327    */
328   public synchronized void shutdown() {
329     scheduler.shutdownNow();
330     if (LOG.isInfoEnabled()) {
331       LOG.info("Chore service for: " + coreThreadPoolPrefix + " had " + scheduledChores.keySet()
332           + " on shutdown");
333     }
334     cancelAllChores(true);
335     scheduledChores.clear();
336     choresMissingStartTime.clear();
337   }
338   
339   /**
340    * @return true when the service is shutdown and thus cannot be used anymore
341    */
342   public boolean isShutdown() {
343     return scheduler.isShutdown();
344   }
345 
346   /**
347    * @return true when the service is shutdown and all threads have terminated
348    */
349   public boolean isTerminated() {
350     return scheduler.isTerminated();
351   }
352 
353   private void cancelAllChores(final boolean mayInterruptIfRunning) {
354     ArrayList<ScheduledChore> choresToCancel = new ArrayList<ScheduledChore>();
355     // Build list of chores to cancel so we can iterate through a set that won't change
356     // as chores are cancelled. If we tried to cancel each chore while iterating through
357     // keySet the results would be undefined because the keySet would be changing
358     for (ScheduledChore chore : scheduledChores.keySet()) {
359       choresToCancel.add(chore);
360     }
361     for (ScheduledChore chore : choresToCancel) {
362       cancelChore(chore, mayInterruptIfRunning);
363     }
364     choresToCancel.clear();
365   }
366 
367   /**
368    * Prints a summary of important details about the chore. Used for debugging purposes
369    */
370   private void printChoreDetails(final String header, ScheduledChore chore) {
371     LinkedHashMap<String, String> output = new LinkedHashMap<String, String>();
372     output.put(header, "");
373     output.put("Chore name: ", chore.getName());
374     output.put("Chore period: ", Integer.toString(chore.getPeriod()));
375     output.put("Chore timeBetweenRuns: ", Long.toString(chore.getTimeBetweenRuns()));
376 
377     for (Entry<String, String> entry : output.entrySet()) {
378       if (LOG.isTraceEnabled()) LOG.trace(entry.getKey() + entry.getValue());
379     }
380   }
381 
382   /**
383    * Prints a summary of important details about the service. Used for debugging purposes
384    */
385   private void printChoreServiceDetails(final String header) {
386     LinkedHashMap<String, String> output = new LinkedHashMap<String, String>();
387     output.put(header, "");
388     output.put("ChoreService corePoolSize: ", Integer.toString(getCorePoolSize()));
389     output.put("ChoreService scheduledChores: ", Integer.toString(getNumberOfScheduledChores()));
390     output.put("ChoreService missingStartTimeCount: ",
391       Integer.toString(getNumberOfChoresMissingStartTime()));
392 
393     for (Entry<String, String> entry : output.entrySet()) {
394       if (LOG.isTraceEnabled()) LOG.trace(entry.getKey() + entry.getValue());
395     }
396   }
397 }