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