001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.util;
019
020import com.google.common.util.concurrent.ThreadFactoryBuilder;
021import org.apache.commons.logging.Log;
022import org.apache.commons.logging.LogFactory;
023
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.Comparator;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030import java.util.concurrent.Executors;
031import java.util.concurrent.ExecutorService;
032import java.util.concurrent.Future;
033import java.util.concurrent.TimeoutException;
034import java.util.concurrent.TimeUnit;
035import java.util.concurrent.atomic.AtomicBoolean;
036
037/**
038 * The <code>ShutdownHookManager</code> enables running shutdownHook
039 * in a deterministic order, higher priority first.
040 * <p/>
041 * The JVM runs ShutdownHooks in a non-deterministic order or in parallel.
042 * This class registers a single JVM shutdownHook and run all the
043 * shutdownHooks registered to it (to this class) in order based on their
044 * priority.
045 */
046public class ShutdownHookManager {
047
048  private static final ShutdownHookManager MGR = new ShutdownHookManager();
049
050  private static final Log LOG = LogFactory.getLog(ShutdownHookManager.class);
051  private static final long TIMEOUT_DEFAULT = 10;
052  private static final TimeUnit TIME_UNIT_DEFAULT = TimeUnit.SECONDS;
053
054  private static final ExecutorService EXECUTOR =
055      Executors.newSingleThreadExecutor(new ThreadFactoryBuilder()
056          .setDaemon(true).build());
057  static {
058    try {
059      Runtime.getRuntime().addShutdownHook(
060        new Thread() {
061          @Override
062          public void run() {
063            MGR.shutdownInProgress.set(true);
064            for (HookEntry entry: MGR.getShutdownHooksInOrder()) {
065              Future<?> future = EXECUTOR.submit(entry.getHook());
066              try {
067                future.get(entry.getTimeout(), entry.getTimeUnit());
068              } catch (TimeoutException ex) {
069                future.cancel(true);
070                LOG.warn("ShutdownHook '" + entry.getHook().getClass().
071                    getSimpleName() + "' timeout, " + ex.toString(), ex);
072              } catch (Throwable ex) {
073                LOG.warn("ShutdownHook '" + entry.getHook().getClass().
074                    getSimpleName() + "' failed, " + ex.toString(), ex);
075              }
076            }
077            try {
078              EXECUTOR.shutdown();
079              if (!EXECUTOR.awaitTermination(TIMEOUT_DEFAULT,
080                  TIME_UNIT_DEFAULT)) {
081                LOG.error("ShutdownHookManger shutdown forcefully.");
082                EXECUTOR.shutdownNow();
083              }
084              LOG.debug("ShutdownHookManger complete shutdown.");
085            } catch (InterruptedException ex) {
086              LOG.error("ShutdownHookManger interrupted while waiting for " +
087                  "termination.", ex);
088              EXECUTOR.shutdownNow();
089              Thread.currentThread().interrupt();
090            }
091          }
092        }
093      );
094    } catch (IllegalStateException ex) {
095      // JVM is being shut down. Ignore
096      LOG.warn("Failed to add the ShutdownHook", ex);
097    }
098  }
099
100  /**
101   * Return <code>ShutdownHookManager</code> singleton.
102   *
103   * @return <code>ShutdownHookManager</code> singleton.
104   */
105  public static ShutdownHookManager get() {
106    return MGR;
107  }
108
109  /**
110   * Private structure to store ShutdownHook, its priority and timeout
111   * settings.
112   */
113  static class HookEntry {
114    private final Runnable hook;
115    private final int priority;
116    private final long timeout;
117    private final TimeUnit unit;
118
119    HookEntry(Runnable hook, int priority) {
120      this(hook, priority, TIMEOUT_DEFAULT, TIME_UNIT_DEFAULT);
121    }
122
123    HookEntry(Runnable hook, int priority, long timeout, TimeUnit unit) {
124      this.hook = hook;
125      this.priority = priority;
126      this.timeout = timeout;
127      this.unit = unit;
128    }
129
130    @Override
131    public int hashCode() {
132      return hook.hashCode();
133    }
134
135    @Override
136    public boolean equals(Object obj) {
137      boolean eq = false;
138      if (obj != null) {
139        if (obj instanceof HookEntry) {
140          eq = (hook == ((HookEntry)obj).hook);
141        }
142      }
143      return eq;
144    }
145
146    Runnable getHook() {
147      return hook;
148    }
149
150    int getPriority() {
151      return priority;
152    }
153
154    long getTimeout() {
155      return timeout;
156    }
157
158    TimeUnit getTimeUnit() {
159      return unit;
160    }
161  }
162
163  private final Set<HookEntry> hooks =
164      Collections.synchronizedSet(new HashSet<HookEntry>());
165
166  private AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
167
168  //private to constructor to ensure singularity
169  private ShutdownHookManager() {
170  }
171
172  /**
173   * Returns the list of shutdownHooks in order of execution,
174   * Highest priority first.
175   *
176   * @return the list of shutdownHooks in order of execution.
177   */
178  List<HookEntry> getShutdownHooksInOrder() {
179    List<HookEntry> list;
180    synchronized (MGR.hooks) {
181      list = new ArrayList<HookEntry>(MGR.hooks);
182    }
183    Collections.sort(list, new Comparator<HookEntry>() {
184
185      //reversing comparison so highest priority hooks are first
186      @Override
187      public int compare(HookEntry o1, HookEntry o2) {
188        return o2.priority - o1.priority;
189      }
190    });
191    return list;
192  }
193
194  /**
195   * Adds a shutdownHook with a priority, the higher the priority
196   * the earlier will run. ShutdownHooks with same priority run
197   * in a non-deterministic order.
198   *
199   * @param shutdownHook shutdownHook <code>Runnable</code>
200   * @param priority priority of the shutdownHook.
201   */
202  public void addShutdownHook(Runnable shutdownHook, int priority) {
203    if (shutdownHook == null) {
204      throw new IllegalArgumentException("shutdownHook cannot be NULL");
205    }
206    if (shutdownInProgress.get()) {
207      throw new IllegalStateException("Shutdown in progress, cannot add a " +
208          "shutdownHook");
209    }
210    hooks.add(new HookEntry(shutdownHook, priority));
211  }
212
213  /**
214   *
215   * Adds a shutdownHook with a priority and timeout the higher the priority
216   * the earlier will run. ShutdownHooks with same priority run
217   * in a non-deterministic order. The shutdown hook will be terminated if it
218   * has not been finished in the specified period of time.
219   *
220   * @param shutdownHook shutdownHook <code>Runnable</code>
221   * @param priority priority of the shutdownHook
222   * @param timeout timeout of the shutdownHook
223   * @param unit unit of the timeout <code>TimeUnit</code>
224   */
225  public void addShutdownHook(Runnable shutdownHook, int priority, long timeout,
226      TimeUnit unit) {
227    if (shutdownHook == null) {
228      throw new IllegalArgumentException("shutdownHook cannot be NULL");
229    }
230    if (shutdownInProgress.get()) {
231      throw new IllegalStateException("Shutdown in progress, cannot add a " +
232          "shutdownHook");
233    }
234    hooks.add(new HookEntry(shutdownHook, priority, timeout, unit));
235  }
236
237  /**
238   * Removes a shutdownHook.
239   *
240   * @param shutdownHook shutdownHook to remove.
241   * @return TRUE if the shutdownHook was registered and removed,
242   * FALSE otherwise.
243   */
244  public boolean removeShutdownHook(Runnable shutdownHook) {
245    if (shutdownInProgress.get()) {
246      throw new IllegalStateException("Shutdown in progress, cannot remove a " +
247          "shutdownHook");
248    }
249    return hooks.remove(new HookEntry(shutdownHook, 0));
250  }
251
252  /**
253   * Indicates if a shutdownHook is registered or not.
254   *
255   * @param shutdownHook shutdownHook to check if registered.
256   * @return TRUE/FALSE depending if the shutdownHook is is registered.
257   */
258  public boolean hasShutdownHook(Runnable shutdownHook) {
259    return hooks.contains(new HookEntry(shutdownHook, 0));
260  }
261  
262  /**
263   * Indicates if shutdown is in progress or not.
264   * 
265   * @return TRUE if the shutdown is in progress, otherwise FALSE.
266   */
267  public boolean isShutdownInProgress() {
268    return shutdownInProgress.get();
269  }
270
271  /**
272   * clear all registered shutdownHooks.
273   */
274  public void clearShutdownHooks() {
275    hooks.clear();
276  }
277}