YARN-679. Add an entry point that can start any Yarn service. Contributed by Steve Loughran.
Project: http://git-wip-us.apache.org/repos/asf/hadoop/repo Commit: http://git-wip-us.apache.org/repos/asf/hadoop/commit/373bb493 Tree: http://git-wip-us.apache.org/repos/asf/hadoop/tree/373bb493 Diff: http://git-wip-us.apache.org/repos/asf/hadoop/diff/373bb493 Branch: refs/heads/HDFS-10467 Commit: 373bb4931fb392e3ca6bfd78992887e5a405e186 Parents: cb672a4 Author: Junping Du <[email protected]> Authored: Fri Apr 28 10:45:02 2017 -0700 Committer: Junping Du <[email protected]> Committed: Fri Apr 28 10:45:02 2017 -0700 ---------------------------------------------------------------------- .../hadoop/service/ServiceStateException.java | 66 +- .../launcher/AbstractLaunchableService.java | 78 ++ .../HadoopUncaughtExceptionHandler.java | 129 +++ .../service/launcher/InterruptEscalator.java | 216 ++++ .../hadoop/service/launcher/IrqHandler.java | 178 +++ .../service/launcher/LaunchableService.java | 95 ++ .../service/launcher/LauncherArguments.java | 59 + .../service/launcher/LauncherExitCodes.java | 183 +++ .../launcher/ServiceLaunchException.java | 81 ++ .../service/launcher/ServiceLauncher.java | 1044 ++++++++++++++++++ .../service/launcher/ServiceShutdownHook.java | 112 ++ .../hadoop/service/launcher/package-info.java | 462 ++++++++ .../apache/hadoop/util/ExitCodeProvider.java | 35 + .../java/org/apache/hadoop/util/ExitUtil.java | 253 ++++- .../hadoop/util/GenericOptionsParser.java | 125 ++- .../org/apache/hadoop/util/StringUtils.java | 43 +- .../apache/hadoop/service/BreakableService.java | 23 +- .../AbstractServiceLauncherTestBase.java | 317 ++++++ .../launcher/ExitTrackingServiceLauncher.java | 59 + .../service/launcher/TestServiceConf.java | 146 +++ .../launcher/TestServiceInterruptHandling.java | 118 ++ .../service/launcher/TestServiceLauncher.java | 213 ++++ .../TestServiceLauncherCreationFailures.java | 83 ++ .../TestServiceLauncherInnerMethods.java | 95 ++ .../ExceptionInExecuteLaunchableService.java | 96 ++ .../testservices/FailInConstructorService.java | 33 + .../testservices/FailInInitService.java | 38 + .../testservices/FailInStartService.java | 37 + .../testservices/FailingStopInStartService.java | 47 + .../testservices/FailureTestService.java | 55 + .../InitInConstructorLaunchableService.java | 63 ++ .../testservices/LaunchableRunningService.java | 111 ++ .../testservices/NoArgsAllowedService.java | 64 ++ .../testservices/NullBindLaunchableService.java | 46 + .../launcher/testservices/RunningService.java | 84 ++ .../StoppingInStartLaunchableService.java | 49 + .../StringConstructorOnlyService.java | 39 + .../apache/hadoop/test/GenericTestUtils.java | 34 + 38 files changed, 4861 insertions(+), 148 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/ServiceStateException.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/ServiceStateException.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/ServiceStateException.java index 0ac1821..ba4e0d2 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/ServiceStateException.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/ServiceStateException.java @@ -20,37 +20,83 @@ package org.apache.hadoop.service; import org.apache.hadoop.classification.InterfaceAudience.Public; import org.apache.hadoop.classification.InterfaceStability.Evolving; +import org.apache.hadoop.service.launcher.LauncherExitCodes; +import org.apache.hadoop.util.ExitCodeProvider; /** - * Exception that is raised on state change operations. + * Exception that can be raised on state change operations, whose + * exit code can be explicitly set, determined from that of any nested + * cause, or a default value of + * {@link LauncherExitCodes#EXIT_SERVICE_LIFECYCLE_EXCEPTION}. */ @Public @Evolving -public class ServiceStateException extends RuntimeException { +public class ServiceStateException extends RuntimeException implements + ExitCodeProvider { private static final long serialVersionUID = 1110000352259232646L; + /** + * Exit code. + */ + private int exitCode ; + + /** + * Instantiate + * @param message error message + */ public ServiceStateException(String message) { - super(message); + this(message, null); } + /** + * Instantiate with a message and cause; if the cause has an exit code + * then it is used, otherwise the generic + * {@link LauncherExitCodes#EXIT_SERVICE_LIFECYCLE_EXCEPTION} exit code + * is used. + * @param message exception message + * @param cause optional inner cause + */ public ServiceStateException(String message, Throwable cause) { super(message, cause); + if(cause instanceof ExitCodeProvider) { + this.exitCode = ((ExitCodeProvider) cause).getExitCode(); + } else { + this.exitCode = LauncherExitCodes.EXIT_SERVICE_LIFECYCLE_EXCEPTION; + } + } + + /** + * Instantiate, using the specified exit code as the exit code + * of the exception, irrespetive of any exit code supplied by any inner + * cause. + * + * @param exitCode exit code to declare + * @param message exception message + * @param cause inner cause + */ + public ServiceStateException(int exitCode, + String message, + Throwable cause) { + this(message, cause); + this.exitCode = exitCode; } public ServiceStateException(Throwable cause) { super(cause); } + @Override + public int getExitCode() { + return exitCode; + } + /** * Convert any exception into a {@link RuntimeException}. - * If the caught exception is already of that type, it is typecast to a - * {@link RuntimeException} and returned. - * * All other exception types are wrapped in a new instance of - * ServiceStateException + * {@code ServiceStateException}. * @param fault exception or throwable - * @return a ServiceStateException to rethrow + * @return a {@link RuntimeException} to rethrow */ public static RuntimeException convert(Throwable fault) { if (fault instanceof RuntimeException) { @@ -66,10 +112,10 @@ public class ServiceStateException extends RuntimeException { * {@link RuntimeException} and returned. * * All other exception types are wrapped in a new instance of - * ServiceStateException + * {@code ServiceStateException}. * @param text text to use if a new exception is created * @param fault exception or throwable - * @return a ServiceStateException to rethrow + * @return a {@link RuntimeException} to rethrow */ public static RuntimeException convert(String text, Throwable fault) { if (fault instanceof RuntimeException) { http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/AbstractLaunchableService.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/AbstractLaunchableService.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/AbstractLaunchableService.java new file mode 100644 index 0000000..be28c5b --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/AbstractLaunchableService.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.AbstractService; + +/** + * Subclass of {@link AbstractService} that provides basic implementations + * of the {@link LaunchableService} methods. + */ [email protected] [email protected] +public abstract class AbstractLaunchableService extends AbstractService + implements LaunchableService { + + private static final Logger LOG = + LoggerFactory.getLogger(AbstractLaunchableService.class); + + /** + * Construct an instance with the given name. + */ + protected AbstractLaunchableService(String name) { + super(name); + } + + /** + * {@inheritDoc} + * <p> + * The base implementation logs all arguments at the debug level, + * then returns the passed in config unchanged. + */ + + @Override + public Configuration bindArgs(Configuration config, List<String> args) throws + Exception { + if (LOG.isDebugEnabled()) { + LOG.debug("Service {} passed in {} arguments:", getName(), args.size()); + for (String arg : args) { + LOG.debug(arg); + } + } + return config; + } + + /** + * {@inheritDoc} + * <p> + * The action is to signal success by returning the exit code 0. + */ + @Override + public int execute() throws Exception { + return LauncherExitCodes.EXIT_SUCCESS; + } +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/HadoopUncaughtExceptionHandler.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/HadoopUncaughtExceptionHandler.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/HadoopUncaughtExceptionHandler.java new file mode 100644 index 0000000..bf4a863 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/HadoopUncaughtExceptionHandler.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +import java.lang.Thread.UncaughtExceptionHandler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.classification.InterfaceAudience.Public; +import org.apache.hadoop.classification.InterfaceStability.Evolving; +import org.apache.hadoop.util.ExitUtil; +import org.apache.hadoop.util.ShutdownHookManager; + +/** + * This class is intended to be installed by calling + * {@link Thread#setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)} + * in the main entry point. + * + * The base class will always attempt to shut down the process if an Error + * was raised; the behavior on a standard Exception, raised outside + * process shutdown, is simply to log it. + * + * (Based on the class {@code YarnUncaughtExceptionHandler}) + */ +@SuppressWarnings("UseOfSystemOutOrSystemErr") +@Public +@Evolving +public class HadoopUncaughtExceptionHandler + implements UncaughtExceptionHandler { + + /** + * Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger( + HadoopUncaughtExceptionHandler.class); + + /** + * Delegate for simple exceptions. + */ + private final UncaughtExceptionHandler delegate; + + /** + * Create an instance delegating to the supplied handler if + * the exception is considered "simple". + * @param delegate a delegate exception handler. + */ + public HadoopUncaughtExceptionHandler(UncaughtExceptionHandler delegate) { + this.delegate = delegate; + } + + /** + * Basic exception handler -logs simple exceptions, then continues. + */ + public HadoopUncaughtExceptionHandler() { + this(null); + } + + /** + * Uncaught exception handler. + * If an error is raised: shutdown + * The state of the system is unknown at this point -attempting + * a clean shutdown is dangerous. Instead: exit + * @param thread thread that failed + * @param exception the raised exception + */ + @Override + public void uncaughtException(Thread thread, Throwable exception) { + if (ShutdownHookManager.get().isShutdownInProgress()) { + LOG.error("Thread {} threw an error during shutdown: {}.", + thread.toString(), + exception, + exception); + } else if (exception instanceof Error) { + try { + LOG.error("Thread {} threw an error: {}. Shutting down", + thread.toString(), + exception, + exception); + } catch (Throwable err) { + // We don't want to not exit because of an issue with logging + } + if (exception instanceof OutOfMemoryError) { + // After catching an OOM java says it is undefined behavior, so don't + // even try to clean up or we can get stuck on shutdown. + try { + System.err.println("Halting due to Out Of Memory Error..."); + } catch (Throwable err) { + // Again we don't want to exit because of logging issues. + } + ExitUtil.haltOnOutOfMemory((OutOfMemoryError) exception); + } else { + // error other than OutOfMemory + ExitUtil.ExitException ee = + ServiceLauncher.convertToExitException(exception); + ExitUtil.terminate(ee.status, ee); + } + } else { + // simple exception in a thread. There's a policy decision here: + // terminate the process vs. keep going after a thread has failed + // base implementation: do nothing but log + LOG.error("Thread {} threw an exception: {}", + thread.toString(), + exception, + exception); + if (delegate != null) { + delegate.uncaughtException(thread, exception); + } + } + + } + +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/InterruptEscalator.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/InterruptEscalator.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/InterruptEscalator.java new file mode 100644 index 0000000..a7e1edd --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/InterruptEscalator.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.service.Service; +import org.apache.hadoop.util.ExitUtil; + +import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_INTERRUPTED; + +/** + * Handles interrupts by shutting down a service, escalating if the service + * does not shut down in time, or when other interrupts are received. + * <ol> + * <li>The service is given a time in milliseconds to stop: + * if it exceeds this it the process exits anyway.</li> + * <li>the exit operation used is {@link ServiceLauncher#exit(int, String)} + * with the exit code {@link LauncherExitCodes#EXIT_INTERRUPTED}</li> + * <li>If a second shutdown signal is received during the shutdown + * process, {@link ExitUtil#halt(int)} is invoked. This handles the + * problem of blocking shutdown hooks.</li> + * </ol> + * + */ + [email protected] [email protected] +public class InterruptEscalator implements IrqHandler.Interrupted { + private static final Logger LOG = LoggerFactory.getLogger( + InterruptEscalator.class); + + /** + * Flag to indicate when a shutdown signal has already been received. + * This allows the operation to be escalated. + */ + private final AtomicBoolean signalAlreadyReceived = new AtomicBoolean(false); + + private final WeakReference<ServiceLauncher> ownerRef; + + private final int shutdownTimeMillis; + + /** + * Previous interrupt handlers. These are not queried. + */ + private final List<IrqHandler> interruptHandlers = new ArrayList<>(2); + private boolean forcedShutdownTimedOut; + + public InterruptEscalator(ServiceLauncher owner, int shutdownTimeMillis) { + Preconditions.checkArgument(owner != null, "null owner"); + this.ownerRef = new WeakReference<>(owner); + this.shutdownTimeMillis = shutdownTimeMillis; + } + + private ServiceLauncher getOwner() { + return ownerRef.get(); + } + + private Service getService() { + ServiceLauncher owner = getOwner(); + return owner != null ? owner.getService() : null; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("InterruptEscalator{"); + sb.append(" signalAlreadyReceived=").append(signalAlreadyReceived.get()); + ServiceLauncher owner = ownerRef.get(); + if (owner != null) { + sb.append(", owner= ").append(owner.toString()); + } + sb.append(", shutdownTimeMillis=").append(shutdownTimeMillis); + sb.append(", forcedShutdownTimedOut=").append(forcedShutdownTimedOut); + sb.append('}'); + return sb.toString(); + } + + @Override + public void interrupted(IrqHandler.InterruptData interruptData) { + String message = "Service interrupted by " + interruptData.toString(); + LOG.warn(message); + if (!signalAlreadyReceived.compareAndSet(false, true)) { + message = "Repeated interrupt: escalating to a JVM halt"; + LOG.warn(message); + // signal already received. On a second request to a hard JVM + // halt and so bypass any blocking shutdown hooks. + ExitUtil.halt(LauncherExitCodes.EXIT_INTERRUPTED, message); + } + Service service = getService(); + if (service != null) { + //start an async shutdown thread with a timeout + ServiceForcedShutdown shutdown = + new ServiceForcedShutdown(service, shutdownTimeMillis); + Thread thread = new Thread(shutdown); + thread.setDaemon(true); + thread.setName("Service Forced Shutdown"); + thread.start(); + //wait for that thread to finish + try { + thread.join(shutdownTimeMillis); + } catch (InterruptedException ignored) { + //ignored + } + forcedShutdownTimedOut = !shutdown.getServiceWasShutdown(); + if (forcedShutdownTimedOut) { + LOG.warn("Service did not shut down in time"); + } + } + ExitUtil.terminate(EXIT_INTERRUPTED, message); + } + + /** + * Register an interrupt handler. + * @param signalName signal name + * @throws IllegalArgumentException if the registration failed + */ + public synchronized void register(String signalName) { + IrqHandler handler = new IrqHandler(signalName, this); + handler.bind(); + interruptHandlers.add(handler); + } + + /** + * Look up the handler for a signal. + * @param signalName signal name + * @return a handler if found + */ + public synchronized IrqHandler lookup(String signalName) { + for (IrqHandler irqHandler : interruptHandlers) { + if (irqHandler.getName().equals(signalName)) { + return irqHandler; + } + } + return null; + } + + /** + * Flag set if forced shut down timed out. + * @return true if a shutdown was attempted and it timed out + */ + public boolean isForcedShutdownTimedOut() { + return forcedShutdownTimedOut; + } + + /** + * Flag set if a signal has been received. + * @return true if there has been one interrupt already. + */ + public boolean isSignalAlreadyReceived() { + return signalAlreadyReceived.get(); + } + + /** + * Forced shutdown runnable. + */ + protected static class ServiceForcedShutdown implements Runnable { + + private final int shutdownTimeMillis; + private final AtomicBoolean serviceWasShutdown = + new AtomicBoolean(false); + private Service service; + + public ServiceForcedShutdown(Service service, int shutdownTimeMillis) { + this.shutdownTimeMillis = shutdownTimeMillis; + this.service = service; + } + + /** + * Shutdown callback: stop the service and set an atomic boolean + * if it stopped within the shutdown time. + */ + @Override + public void run() { + if (service != null) { + service.stop(); + serviceWasShutdown.set( + service.waitForServiceToStop(shutdownTimeMillis)); + } else { + serviceWasShutdown.set(true); + } + } + + /** + * Probe for the service being shutdown. + * @return true if the service has been shutdown in the runnable + */ + private boolean getServiceWasShutdown() { + return serviceWasShutdown.get(); + } + } +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/IrqHandler.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/IrqHandler.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/IrqHandler.java new file mode 100644 index 0000000..30bb91c --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/IrqHandler.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sun.misc.Signal; +import sun.misc.SignalHandler; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Handler of interrupts that relays them to a registered + * implementation of {@link IrqHandler.Interrupted}. + * + * This class bundles up all the compiler warnings about abuse of sun.misc + * interrupt handling code into one place. + */ [email protected] [email protected] +@SuppressWarnings("UseOfSunClasses") +public final class IrqHandler implements SignalHandler { + private static final Logger LOG = LoggerFactory.getLogger(IrqHandler.class); + + /** + * Definition of the Control-C handler name: {@value}. + */ + public static final String CONTROL_C = "INT"; + + /** + * Definition of default <code>kill</code> signal: {@value}. + */ + public static final String SIGTERM = "TERM"; + + /** + * Signal name. + */ + private final String name; + + /** + * Handler to relay to. + */ + private final Interrupted handler; + + /** Count of how many times a signal has been raised. */ + private final AtomicInteger signalCount = new AtomicInteger(0); + + /** + * Stored signal. + */ + private Signal signal; + + /** + * Create an IRQ handler bound to the specific interrupt. + * @param name signal name + * @param handler handler + */ + public IrqHandler(String name, Interrupted handler) { + Preconditions.checkArgument(name != null, "Null \"name\""); + Preconditions.checkArgument(handler != null, "Null \"handler\""); + this.handler = handler; + this.name = name; + } + + /** + * Bind to the interrupt handler. + * @throws IllegalArgumentException if the exception could not be set + */ + void bind() { + Preconditions.checkState(signal == null, "Handler already bound"); + try { + signal = new Signal(name); + Signal.handle(signal, this); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Could not set handler for signal \"" + name + "\"." + + "This can happen if the JVM has the -Xrs set.", + e); + } + } + + /** + * @return the signal name. + */ + public String getName() { + return name; + } + + /** + * Raise the signal. + */ + public void raise() { + Signal.raise(signal); + } + + @Override + public String toString() { + return "IrqHandler for signal " + name; + } + + /** + * Handler for the JVM API for signal handling. + * @param s signal raised + */ + @Override + public void handle(Signal s) { + signalCount.incrementAndGet(); + InterruptData data = new InterruptData(s.getName(), s.getNumber()); + LOG.info("Interrupted: {}", data); + handler.interrupted(data); + } + + /** + * Get the count of how many times a signal has been raised. + * @return the count of signals + */ + public int getSignalCount() { + return signalCount.get(); + } + + /** + * Callback issues on an interrupt. + */ + public interface Interrupted { + + /** + * Handle an interrupt. + * @param interruptData data + */ + void interrupted(InterruptData interruptData); + } + + /** + * Interrupt data to pass on. + */ + public static class InterruptData { + private final String name; + private final int number; + + public InterruptData(String name, int number) { + this.name = name; + this.number = number; + } + + public String getName() { + return name; + } + + public int getNumber() { + return number; + } + + @Override + public String toString() { + return "signal " + name + '(' + number + ')'; + } + } +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LaunchableService.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LaunchableService.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LaunchableService.java new file mode 100644 index 0000000..fb0a052 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LaunchableService.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +import java.util.List; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.Service; + +/** + * An interface which services can implement to have their + * execution managed by the ServiceLauncher. + * <p> + * The command line options will be passed down before the + * {@link Service#init(Configuration)} operation is invoked via an + * invocation of {@link LaunchableService#bindArgs(Configuration, List)} + * After the service has been successfully started via {@link Service#start()} + * the {@link LaunchableService#execute()} method is called to execute the + * service. When this method returns, the service launcher will exit, using + * the return code from the method as its exit option. + */ [email protected] [email protected] +public interface LaunchableService extends Service { + + /** + * Propagate the command line arguments. + * <p> + * This method is called before {@link #init(Configuration)}; + * Any non-null configuration that is returned from this operation + * becomes the one that is passed on to that {@link #init(Configuration)} + * operation. + * <p> + * This permits implementations to change the configuration before + * the init operation. As the ServiceLauncher only creates + * an instance of the base {@link Configuration} class, it is + * recommended to instantiate any subclass (such as YarnConfiguration) + * that injects new resources. + * <p> + * @param config the initial configuration build up by the + * service launcher. + * @param args list of arguments passed to the command line + * after any launcher-specific commands have been stripped. + * @return the configuration to init the service with. + * Recommended: pass down the config parameter with any changes + * @throws Exception any problem + */ + Configuration bindArgs(Configuration config, List<String> args) + throws Exception; + + /** + * Run a service. This method is called after {@link Service#start()}. + * <p> + * The return value becomes the exit code of the launched process. + * <p> + * If an exception is raised, the policy is: + * <ol> + * <li>Any subset of {@link org.apache.hadoop.util.ExitUtil.ExitException}: + * the exception is passed up unmodified. + * </li> + * <li>Any exception which implements + * {@link org.apache.hadoop.util.ExitCodeProvider}: + * A new {@link ServiceLaunchException} is created with the exit code + * and message of the thrown exception; the thrown exception becomes the + * cause.</li> + * <li>Any other exception: a new {@link ServiceLaunchException} is created + * with the exit code {@link LauncherExitCodes#EXIT_EXCEPTION_THROWN} and + * the message of the original exception (which becomes the cause).</li> + * </ol> + * @return the exit code + * @throws org.apache.hadoop.util.ExitUtil.ExitException an exception passed + * up as the exit code and error text. + * @throws Exception any exception to report. If it provides an exit code + * this is used in a wrapping exception. + */ + int execute() throws Exception; +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherArguments.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherArguments.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherArguments.java new file mode 100644 index 0000000..08118f5 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherArguments.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +/** + * Standard launcher arguments. These are all from + * the {@code GenericOptionsParser}, simply extracted to constants. + */ +public interface LauncherArguments { + /** + * Name of the configuration argument on the CLI. + * Value: {@value} + */ + String ARG_CONF = "conf"; + String ARG_CONF_SHORT = "conf"; + + /** + * prefixed version of {@link #ARG_CONF}. + * Value: {@value} + */ + String ARG_CONF_PREFIXED = "--" + ARG_CONF; + + /** + * Name of a configuration class which is loaded before any + * attempt is made to load the class. + * <p> + * Value: {@value} + */ + String ARG_CONFCLASS = "hadoopconf"; + String ARG_CONFCLASS_SHORT = "hadoopconf"; + + /** + * Prefixed version of {@link #ARG_CONFCLASS}. + * Value: {@value} + */ + String ARG_CONFCLASS_PREFIXED = "--" + ARG_CONFCLASS; + + /** + * Error string on a parse failure. + * Value: {@value} + */ + String E_PARSE_FAILED = "Failed to parse: "; +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherExitCodes.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherExitCodes.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherExitCodes.java new file mode 100644 index 0000000..f48e38e --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherExitCodes.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Common Exit codes. + * <p> + * Codes with a YARN prefix are YARN-related. + * <p> + * Many of the exit codes are designed to resemble HTTP error codes, + * squashed into a single byte. e.g 44 , "not found" is the equivalent + * of 404. The various 2XX HTTP error codes aren't followed; + * the Unix standard of "0" for success is used. + * <pre> + * 0-10: general command issues + * 30-39: equivalent to the 3XX responses, where those responses are + * considered errors by the application. + * 40-49: client-side/CLI/config problems + * 50-59: service-side problems. + * 60+ : application specific error codes + * </pre> + */ [email protected] [email protected] +public interface LauncherExitCodes { + + /** + * Success: {@value}. + */ + int EXIT_SUCCESS = 0; + + /** + * Generic "false/fail" response: {@value}. + * The operation worked but the result was not "true" from the viewpoint + * of the executed code. + */ + int EXIT_FAIL = -1; + + /** + * Exit code when a client requested service termination: {@value}. + */ + int EXIT_CLIENT_INITIATED_SHUTDOWN = 1; + + /** + * Exit code when targets could not be launched: {@value}. + */ + int EXIT_TASK_LAUNCH_FAILURE = 2; + + /** + * Exit code when a control-C, kill -3, signal was picked up: {@value}. + */ + int EXIT_INTERRUPTED = 3; + + /** + * Exit code when something happened but we can't be specific: {@value}. + */ + int EXIT_OTHER_FAILURE = 5; + + /** + * Exit code when the command line doesn't parse: {@value}, or + * when it is otherwise invalid. + * <p> + * Approximate HTTP equivalent: {@code 400 Bad Request} + */ + int EXIT_COMMAND_ARGUMENT_ERROR = 40; + + /** + * The request requires user authentication: {@value}. + * <p> + * approximate HTTP equivalent: Approximate HTTP equivalent: {@code 401 Unauthorized} + */ + int EXIT_UNAUTHORIZED = 41; + + /** + * Exit code when a usage message was printed: {@value}. + */ + int EXIT_USAGE = 42; + + /** + * Forbidden action: {@value}. + * <p> + * Approximate HTTP equivalent: Approximate HTTP equivalent: {@code 403: Forbidden} + */ + int EXIT_FORBIDDEN = 43; + + /** + * Something was not found: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 404: Not Found} + */ + int EXIT_NOT_FOUND = 44; + + /** + * The operation is not allowed: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 405: Not allowed} + */ + int EXIT_OPERATION_NOT_ALLOWED = 45; + + /** + * The command is somehow not acceptable: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 406: Not Acceptable} + */ + int EXIT_NOT_ACCEPTABLE = 46; + + /** + * Exit code on connectivity problems: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 408: Request Timeout} + */ + int EXIT_CONNECTIVITY_PROBLEM = 48; + + /** + * Exit code when the configurations in valid/incomplete: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 409: Conflict} + */ + int EXIT_BAD_CONFIGURATION = 49; + + /** + * Exit code when an exception was thrown from the service: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 500 Internal Server Error} + */ + int EXIT_EXCEPTION_THROWN = 50; + + /** + * Unimplemented feature: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 501: Not Implemented} + */ + int EXIT_UNIMPLEMENTED = 51; + + /** + * Service Unavailable; it may be available later: {@value}. + * <p> + * Approximate HTTP equivalent: {@code 503 Service Unavailable} + */ + int EXIT_SERVICE_UNAVAILABLE = 53; + + /** + * The application does not support, or refuses to support this + * version: {@value}. + * <p> + * If raised, this is expected to be raised server-side and likely due + * to client/server version incompatibilities. + * <p> + * Approximate HTTP equivalent: {@code 505: Version Not Supported} + */ + int EXIT_UNSUPPORTED_VERSION = 55; + + /** + * The service instance could not be created: {@value}. + */ + int EXIT_SERVICE_CREATION_FAILURE = 56; + + /** + * The service instance could not be created: {@value}. + */ + int EXIT_SERVICE_LIFECYCLE_EXCEPTION = 57; + +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLaunchException.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLaunchException.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLaunchException.java new file mode 100644 index 0000000..1243a1f --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLaunchException.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + + +import java.util.Locale; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.util.ExitCodeProvider; +import org.apache.hadoop.util.ExitUtil; + +/** + * A service launch exception that includes an exit code. + * <p> + * When caught by the ServiceLauncher, it will convert that + * into a process exit code. + * + * The {@link #ServiceLaunchException(int, String, Object...)} constructor + * generates formatted exceptions. + */ [email protected] [email protected] + +public class ServiceLaunchException extends ExitUtil.ExitException + implements ExitCodeProvider, LauncherExitCodes { + + /** + * Create an exception with the specific exit code. + * @param exitCode exit code + * @param cause cause of the exception + */ + public ServiceLaunchException(int exitCode, Throwable cause) { + super(exitCode, cause); + } + + /** + * Create an exception with the specific exit code and text. + * @param exitCode exit code + * @param message message to use in exception + */ + public ServiceLaunchException(int exitCode, String message) { + super(exitCode, message); + } + + /** + * Create a formatted exception. + * <p> + * This uses {@link String#format(String, Object...)} + * to build the formatted exception in the ENGLISH locale. + * <p> + * If the last argument is a throwable, it becomes the cause of the exception. + * It will also be used as a parameter for the format. + * @param exitCode exit code + * @param format format for message to use in exception + * @param args list of arguments + */ + public ServiceLaunchException(int exitCode, String format, Object... args) { + super(exitCode, String.format(Locale.ENGLISH, format, args)); + if (args.length > 0 && (args[args.length - 1] instanceof Throwable)) { + initCause((Throwable) args[args.length - 1]); + } + } + +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLauncher.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLauncher.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLauncher.java new file mode 100644 index 0000000..6b0b4e8 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLauncher.java @@ -0,0 +1,1044 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionBuilder; +import org.apache.commons.cli.Options; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.net.NetUtils; +import org.apache.hadoop.service.Service; +import org.apache.hadoop.util.ExitCodeProvider; +import org.apache.hadoop.util.ExitUtil; +import org.apache.hadoop.util.GenericOptionsParser; +import org.apache.hadoop.util.StringUtils; + +/** + * A class to launch any YARN service by name. + * + * It's designed to be subclassed for custom entry points. + * + * Workflow: + * <ol> + * <li>An instance of the class is created. It must be of the type + * {@link Service}</li> + * <li>If it implements + * {@link LaunchableService#bindArgs(Configuration, List)}, + * it is given the binding args off the CLI after all general configuration + * arguments have been stripped.</li> + * <li>Its {@link Service#init(Configuration)} and {@link Service#start()} + * methods are called.</li> + * <li>If it implements it, {@link LaunchableService#execute()} + * is called and its return code used as the exit code.</li> + * <li>Otherwise: it waits for the service to stop, assuming that the + * {@link Service#start()} method spawns one or more thread + * to perform work</li> + * <li>If any exception is raised and provides an exit code, + * that is, it implements {@link ExitCodeProvider}, + * the return value of {@link ExitCodeProvider#getExitCode()}, + * becomes the exit code of the command.</li> + * </ol> + * Error and warning messages are logged to {@code stderr}. + * + * @param <S> service class to cast the generated service to. + */ +@SuppressWarnings("UseOfSystemOutOrSystemErr") +public class ServiceLauncher<S extends Service> + implements LauncherExitCodes, LauncherArguments, + Thread.UncaughtExceptionHandler { + + /** + * Logger. + */ + private static final Logger LOG = + LoggerFactory.getLogger(ServiceLauncher.class); + + /** + * Priority for the shutdown hook: {@value}. + */ + protected static final int SHUTDOWN_PRIORITY = 30; + + /** + * The name of this class. + */ + public static final String NAME = "ServiceLauncher"; + + protected static final String USAGE_NAME = "Usage: " + NAME; + protected static final String USAGE_SERVICE_ARGUMENTS = + "service-classname <service arguments>"; + + /** + * Usage message. + * + * Text: {@value} + */ + public static final String USAGE_MESSAGE = + USAGE_NAME + + " [" + ARG_CONF_PREFIXED + " <conf file>]" + + " [" + ARG_CONFCLASS_PREFIXED + " <configuration classname>]" + + " " + USAGE_SERVICE_ARGUMENTS; + + /** + * The shutdown time on an interrupt: {@value}. + */ + private static final int SHUTDOWN_TIME_ON_INTERRUPT = 30 * 1000; + + /** + * The launched service. + * + * Invalid until the service has been created. + */ + private volatile S service; + + /** + * Exit code of the service. + * + * Invalid until a service has + * executed or stopped, depending on the service type. + */ + private int serviceExitCode; + + /** + * Any exception raised during execution. + */ + private ExitUtil.ExitException serviceException; + + /** + * The interrupt escalator for the service. + */ + private InterruptEscalator interruptEscalator; + + /** + * Configuration used for the service. + */ + private Configuration configuration; + + /** + * Text description of service for messages. + */ + private String serviceName; + + /** + * Classname for the service to create.; empty string otherwise. + */ + private String serviceClassName = ""; + + /** + * List of the standard configurations to create (and so load in properties). + * The values are Hadoop, HDFS and YARN configurations. + */ + protected static final String[] DEFAULT_CONFIGS = { + "org.apache.hadoop.conf.Configuration", + "org.apache.hadoop.hdfs.HdfsConfiguration", + "org.apache.hadoop.yarn.conf.YarnConfiguration" + }; + + /** + * List of classnames to load to configuration before creating a + * {@link Configuration} instance. + */ + private List<String> confClassnames = new ArrayList<>(DEFAULT_CONFIGS.length); + + /** + * URLs of configurations to load into the configuration instance created. + */ + private List<URL> confResourceUrls = new ArrayList<>(1); + + /** Command options. Preserved for usage statements. */ + private Options commandOptions; + + /** + * Create an instance of the launcher. + * @param serviceClassName classname of the service + */ + public ServiceLauncher(String serviceClassName) { + this(serviceClassName, serviceClassName); + } + + /** + * Create an instance of the launcher. + * @param serviceName name of service for text messages + * @param serviceClassName classname of the service + */ + public ServiceLauncher(String serviceName, String serviceClassName) { + this.serviceClassName = serviceClassName; + this.serviceName = serviceName; + // set up initial list of configurations + confClassnames.addAll(Arrays.asList(DEFAULT_CONFIGS)); + } + + /** + * Get the service. + * + * Null until + * {@link #coreServiceLaunch(Configuration, List, boolean, boolean)} + * has completed. + * @return the service + */ + public final S getService() { + return service; + } + + /** + * Setter is to give subclasses the ability to manipulate the service. + * @param s the new service + */ + protected void setService(S s) { + this.service = s; + } + + /** + * Get the configuration constructed from the command line arguments. + * @return the configuration used to create the service + */ + public final Configuration getConfiguration() { + return configuration; + } + + /** + * The exit code from a successful service execution. + * @return the exit code. + */ + public final int getServiceExitCode() { + return serviceExitCode; + } + + /** + * Get the exit exception used to end this service. + * @return an exception, which will be null until the service + * has exited (and {@code System.exit} has not been called) + */ + public final ExitUtil.ExitException getServiceException() { + return serviceException; + } + + /** + * Probe for service classname being defined. + * @return true if the classname is set + */ + private boolean isClassnameDefined() { + return serviceClassName != null && !serviceClassName.isEmpty(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("\"ServiceLauncher for \""); + sb.append(serviceName); + if (isClassnameDefined()) { + sb.append(", serviceClassName='").append(serviceClassName).append('\''); + } + if (service != null) { + sb.append(", service=").append(service); + } + return sb.toString(); + } + + /** + * Launch the service and exit. + * + * <ol> + * <li>Parse the command line.</li> + * <li>Build the service configuration from it.</li> + * <li>Start the service.</li>. + * <li>If it is a {@link LaunchableService}: execute it</li> + * <li>Otherwise: wait for it to finish.</li> + * <li>Exit passing the status code to the {@link #exit(int, String)} + * method.</li> + * </ol> + * @param args arguments to the service. {@code arg[0]} is + * assumed to be the service classname. + */ + public void launchServiceAndExit(List<String> args) { + StringBuilder builder = new StringBuilder(); + for (String arg : args) { + builder.append('"').append(arg).append("\" "); + } + String argumentString = builder.toString(); + if (LOG.isDebugEnabled()) { + LOG.debug(startupShutdownMessage(serviceName, args)); + LOG.debug(argumentString); + } + registerFailureHandling(); + // set up the configs, using reflection to push in the -site.xml files + loadConfigurationClasses(); + Configuration conf = createConfiguration(); + for (URL resourceUrl : confResourceUrls) { + conf.addResource(resourceUrl); + } + bindCommandOptions(); + ExitUtil.ExitException exitException; + try { + List<String> processedArgs = extractCommandOptions(conf, args); + exitException = launchService(conf, processedArgs, true, true); + } catch (ExitUtil.ExitException e) { + exitException = e; + noteException(exitException); + } + if (exitException.getExitCode() != 0) { + // something went wrong. Print the usage and commands + System.err.println(getUsageMessage()); + System.err.println("Command: " + argumentString); + } + System.out.flush(); + System.err.flush(); + exit(exitException); + } + + /** + * Set the {@link #commandOptions} field to the result of + * {@link #createOptions()}; protected for subclasses and test access. + */ + protected void bindCommandOptions() { + commandOptions = createOptions(); + } + + /** + * Record that an Exit Exception has been raised. + * Save it to {@link #serviceException}, with its exit code in + * {@link #serviceExitCode} + * @param exitException exception + */ + void noteException(ExitUtil.ExitException exitException) { + LOG.debug("Exception raised", exitException); + serviceExitCode = exitException.getExitCode(); + serviceException = exitException; + } + + /** + * Get the usage message, ideally dynamically. + * @return the usage message + */ + protected String getUsageMessage() { + String message = USAGE_MESSAGE; + if (commandOptions != null) { + message = USAGE_NAME + + " " + commandOptions.toString() + + " " + USAGE_SERVICE_ARGUMENTS; + } + return message; + } + + /** + * Override point: create an options instance to combine with the + * standard options set. + * <i>Important. Synchronize uses of {@link OptionBuilder}</i> + * with {@code OptionBuilder.class} + * @return the new options + */ + @SuppressWarnings("static-access") + protected Options createOptions() { + synchronized (OptionBuilder.class) { + Options options = new Options(); + Option oconf = OptionBuilder.withArgName("configuration file") + .hasArg() + .withDescription("specify an application configuration file") + .withLongOpt(ARG_CONF) + .create(ARG_CONF_SHORT); + Option confclass = OptionBuilder.withArgName("configuration classname") + .hasArg() + .withDescription( + "Classname of a Hadoop Configuration subclass to load") + .withLongOpt(ARG_CONFCLASS) + .create(ARG_CONFCLASS_SHORT); + Option property = OptionBuilder.withArgName("property=value") + .hasArg() + .withDescription("use value for given property") + .create('D'); + options.addOption(oconf); + options.addOption(property); + options.addOption(confclass); + return options; + } + } + + /** + * Override point: create the base configuration for the service. + * + * Subclasses can override to create HDFS/YARN configurations etc. + * @return the configuration to use as the service initializer. + */ + protected Configuration createConfiguration() { + return new Configuration(); + } + + /** + * Override point: Get a list of configuration classes to create. + * @return the array of configs to attempt to create. If any are off the + * classpath, that is logged + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + protected List<String> getConfigurationsToCreate() { + return confClassnames; + } + + /** + * This creates all the configurations defined by + * {@link #getConfigurationsToCreate()} , ensuring that + * the resources have been pushed in. + * If one cannot be loaded it is logged and the operation continues + * except in the case that the class does load but it isn't actually + * a subclass of {@link Configuration}. + * @throws ExitUtil.ExitException if a loaded class is of the wrong type + */ + @VisibleForTesting + public int loadConfigurationClasses() { + List<String> toCreate = getConfigurationsToCreate(); + int loaded = 0; + for (String classname : toCreate) { + try { + Class<?> loadClass = getClassLoader().loadClass(classname); + Object instance = loadClass.getConstructor().newInstance(); + if (!(instance instanceof Configuration)) { + throw new ExitUtil.ExitException(EXIT_SERVICE_CREATION_FAILURE, + "Could not create " + classname + + " because it is not a Configuration class/subclass"); + } + loaded++; + } catch (ClassNotFoundException e) { + // class could not be found -implies it is not on the current classpath + LOG.debug("Failed to load {} because it is not on the classpath", + classname); + } catch (ExitUtil.ExitException e) { + // rethrow + throw e; + } catch (Exception e) { + // any other exception + LOG.info("Failed to create {}", classname, e); + } + } + return loaded; + } + + /** + * Launch a service catching all exceptions and downgrading them to exit codes + * after logging. + * + * Sets {@link #serviceException} to this value. + * @param conf configuration to use + * @param processedArgs command line after the launcher-specific arguments + * have been stripped out. + * @param addShutdownHook should a shutdown hook be added to terminate + * this service on shutdown. Tests should set this to false. + * @param execute execute/wait for the service to stop. + * @return an exit exception, which will have a status code of 0 if it worked + */ + @VisibleForTesting + public ExitUtil.ExitException launchService(Configuration conf, + List<String> processedArgs, + boolean addShutdownHook, + boolean execute) { + + ExitUtil.ExitException exitException; + + try { + int exitCode = coreServiceLaunch(conf, processedArgs, addShutdownHook, + execute); + if (service != null) { + // check to see if the service failed + Throwable failure = service.getFailureCause(); + if (failure != null) { + // the service exited with a failure. + // check what state it is in + Service.STATE failureState = service.getFailureState(); + if (failureState == Service.STATE.STOPPED) { + // the failure occurred during shutdown, not important enough + // to bother the user as it may just scare them + LOG.debug("Failure during shutdown: {} ", failure, failure); + } else { + //throw it for the catch handlers to deal with + throw failure; + } + } + } + String name = getServiceName(); + + if (exitCode == 0) { + exitException = new ServiceLaunchException(exitCode, + "%s succeeded", + name); + } else { + exitException = new ServiceLaunchException(exitCode, + "%s failed ", name); + } + // either the service succeeded, or an error raised during shutdown, + // which we don't worry that much about + } catch (ExitUtil.ExitException ee) { + // exit exceptions are passed through unchanged + exitException = ee; + } catch (Throwable thrown) { + exitException = convertToExitException(thrown); + } + noteException(exitException); + return exitException; + } + + /** + * Launch the service. + * + * All exceptions that occur are propagated upwards. + * + * If the method returns a status code, it means that it got as far starting + * the service, and if it implements {@link LaunchableService}, that the + * method {@link LaunchableService#execute()} has completed. + * + * After this method returns, the service can be retrieved returned by + * {@link #getService()}. + * + * @param conf configuration + * @param processedArgs arguments after the configuration parameters + * have been stripped out. + * @param addShutdownHook should a shutdown hook be added to terminate + * this service on shutdown. Tests should set this to false. + * @param execute execute/wait for the service to stop + * @throws ClassNotFoundException classname not on the classpath + * @throws IllegalAccessException not allowed at the class + * @throws InstantiationException not allowed to instantiate it + * @throws InterruptedException thread interrupted + * @throws ExitUtil.ExitException any exception defining the status code. + * @throws Exception any other failure -if it implements + * {@link ExitCodeProvider} then it defines the exit code for any + * containing exception + */ + + protected int coreServiceLaunch(Configuration conf, + List<String> processedArgs, + boolean addShutdownHook, + boolean execute) throws Exception { + + // create the service instance + instantiateService(conf); + ServiceShutdownHook shutdownHook = null; + + // and the shutdown hook if requested + if (addShutdownHook) { + shutdownHook = new ServiceShutdownHook(service); + shutdownHook.register(SHUTDOWN_PRIORITY); + } + String name = getServiceName(); + LOG.debug("Launched service {}", name); + LaunchableService launchableService = null; + + if (service instanceof LaunchableService) { + // it's a LaunchableService, pass in the conf and arguments before init) + LOG.debug("Service {} implements LaunchableService", name); + launchableService = (LaunchableService) service; + if (launchableService.isInState(Service.STATE.INITED)) { + LOG.warn("LaunchableService {}" + + " initialized in constructor before CLI arguments passed in", + name); + } + Configuration newconf = launchableService.bindArgs(configuration, + processedArgs); + if (newconf != null) { + configuration = newconf; + } + } + + //some class constructors init; here this is picked up on. + if (!service.isInState(Service.STATE.INITED)) { + service.init(configuration); + } + int exitCode; + + try { + // start the service + service.start(); + exitCode = EXIT_SUCCESS; + if (execute && service.isInState(Service.STATE.STARTED)) { + if (launchableService != null) { + // assume that runnable services are meant to run from here + try { + exitCode = launchableService.execute(); + LOG.debug("Service {} execution returned exit code {}", + name, exitCode); + } finally { + // then stop the service + service.stop(); + } + } else { + //run the service until it stops or an interrupt happens + // on a different thread. + LOG.debug("waiting for service threads to terminate"); + service.waitForServiceToStop(0); + } + } + } finally { + if (shutdownHook != null) { + shutdownHook.unregister(); + } + } + return exitCode; + } + + /** + * Instantiate the service defined in {@code serviceClassName}. + * + * Sets the {@code configuration} field + * to the the value of {@code conf}, + * and the {@code service} field to the service created. + * + * @param conf configuration to use + */ + @SuppressWarnings("unchecked") + public Service instantiateService(Configuration conf) { + Preconditions.checkArgument(conf != null, "null conf"); + Preconditions.checkArgument(serviceClassName != null, + "null service classname"); + Preconditions.checkArgument(!serviceClassName.isEmpty(), + "undefined service classname"); + configuration = conf; + + // Instantiate the class. this requires the service to have a public + // zero-argument or string-argument constructor + Object instance; + try { + Class<?> serviceClass = getClassLoader().loadClass(serviceClassName); + try { + instance = serviceClass.getConstructor().newInstance(); + } catch (NoSuchMethodException noEmptyConstructor) { + // no simple constructor, fall back to a string + LOG.debug("No empty constructor {}", noEmptyConstructor, + noEmptyConstructor); + instance = serviceClass.getConstructor(String.class) + .newInstance(serviceClassName); + } + } catch (Exception e) { + throw serviceCreationFailure(e); + } + if (!(instance instanceof Service)) { + //not a service + throw new ServiceLaunchException( + LauncherExitCodes.EXIT_SERVICE_CREATION_FAILURE, + "Not a service class: \"%s\"", serviceClassName); + } + + // cast to the specific instance type of this ServiceLauncher + service = (S) instance; + return service; + } + + /** + * Convert an exception to an {@code ExitException}. + * + * This process may just be a simple pass through, otherwise a new + * exception is created with an exit code, the text of the supplied + * exception, and the supplied exception as an inner cause. + * + * <ol> + * <li>If is already the right type, pass it through.</li> + * <li>If it implements {@link ExitCodeProvider#getExitCode()}, + * the exit code is extracted and used in the new exception.</li> + * <li>Otherwise, the exit code + * {@link LauncherExitCodes#EXIT_EXCEPTION_THROWN} is used.</li> + * </ol> + * + * @param thrown the exception thrown + * @return an {@code ExitException} with a status code + */ + protected static ExitUtil.ExitException convertToExitException( + Throwable thrown) { + ExitUtil.ExitException exitException; + // get the exception message + String message = thrown.toString(); + int exitCode; + if (thrown instanceof ExitCodeProvider) { + // the exception provides a status code -extract it + exitCode = ((ExitCodeProvider) thrown).getExitCode(); + message = thrown.getMessage(); + if (message == null) { + // some exceptions do not have a message; fall back + // to the string value. + message = thrown.toString(); + } + } else { + // no exception code: use the default + exitCode = EXIT_EXCEPTION_THROWN; + } + // construct the new exception with the original message and + // an exit code + exitException = new ServiceLaunchException(exitCode, message); + exitException.initCause(thrown); + return exitException; + } + + /** + * Generate an exception announcing a failure to create the service. + * @param exception inner exception. + * @return a new exception, with the exit code + * {@link LauncherExitCodes#EXIT_SERVICE_CREATION_FAILURE} + */ + protected ServiceLaunchException serviceCreationFailure(Exception exception) { + return new ServiceLaunchException(EXIT_SERVICE_CREATION_FAILURE, exception); + } + + /** + * Override point: register this class as the handler for the control-C + * and SIGINT interrupts. + * + * Subclasses can extend this with extra operations, such as + * an exception handler: + * <pre> + * Thread.setDefaultUncaughtExceptionHandler( + * new YarnUncaughtExceptionHandler()); + * </pre> + */ + protected void registerFailureHandling() { + try { + interruptEscalator = new InterruptEscalator(this, + SHUTDOWN_TIME_ON_INTERRUPT); + interruptEscalator.register(IrqHandler.CONTROL_C); + interruptEscalator.register(IrqHandler.SIGTERM); + } catch (IllegalArgumentException e) { + // downgrade interrupt registration to warnings + LOG.warn("{}", e, e); + } + Thread.setDefaultUncaughtExceptionHandler( + new HadoopUncaughtExceptionHandler(this)); + } + + /** + * Handler for uncaught exceptions: terminate the service. + * @param thread thread + * @param exception exception + */ + @Override + public void uncaughtException(Thread thread, Throwable exception) { + LOG.error("Uncaught exception in thread {} -exiting", thread, exception); + exit(convertToExitException(exception)); + } + + /** + * Get the service name via {@link Service#getName()}. + * + * If the service is not instantiated, the classname is returned instead. + * @return the service name + */ + public String getServiceName() { + Service s = service; + String name = null; + if (s != null) { + try { + name = s.getName(); + } catch (Exception ignored) { + // ignored + } + } + if (name != null) { + return "service " + name; + } else { + return "service " + serviceName; + } + } + + /** + * Print a warning message. + * <p> + * This tries to log to the log's warn() operation. + * If the log at that level is disabled it logs to system error + * @param text warning text + */ + protected void warn(String text) { + if (LOG.isWarnEnabled()) { + LOG.warn(text); + } else { + System.err.println(text); + } + } + + /** + * Report an error. + * <p> + * This tries to log to {@code LOG.error()}. + * <p> + * If that log level is disabled disabled the message + * is logged to system error along with {@code thrown.toString()} + * @param message message for the user + * @param thrown the exception thrown + */ + protected void error(String message, Throwable thrown) { + String text = "Exception: " + message; + if (LOG.isErrorEnabled()) { + LOG.error(text, thrown); + } else { + System.err.println(text); + if (thrown != null) { + System.err.println(thrown.toString()); + } + } + } + + /** + * Exit the JVM. + * + * This is method can be overridden for testing, throwing an + * exception instead. Any subclassed method MUST raise an + * {@code ExitException} instance/subclass. + * The service launcher code assumes that after this method is invoked, + * no other code in the same method is called. + * @param exitCode code to exit + */ + protected void exit(int exitCode, String message) { + ExitUtil.terminate(exitCode, message); + } + + /** + * Exit the JVM using an exception for the exit code and message, + * invoking {@link ExitUtil#terminate(ExitUtil.ExitException)}. + * + * This is the standard way a launched service exits. + * An error code of 0 means success -nothing is printed. + * + * If {@link ExitUtil#disableSystemExit()} has been called, this + * method will throw the exception. + * + * The method <i>may</i> be subclassed for testing + * @param ee exit exception + * @throws ExitUtil.ExitException if ExitUtil exceptions are disabled + */ + protected void exit(ExitUtil.ExitException ee) { + ExitUtil.terminate(ee); + } + + /** + * Override point: get the classloader to use. + * @return the classloader for loading a service class. + */ + protected ClassLoader getClassLoader() { + return this.getClass().getClassLoader(); + } + + /** + * Extract the command options and apply them to the configuration, + * building an array of processed arguments to hand down to the service. + * + * @param conf configuration to update. + * @param args main arguments. {@code args[0]}is assumed to be + * the service classname and is skipped. + * @return the remaining arguments + * @throws ExitUtil.ExitException if JVM exiting is disabled. + */ + public List<String> extractCommandOptions(Configuration conf, + List<String> args) { + int size = args.size(); + if (size <= 1) { + return new ArrayList<>(0); + } + List<String> coreArgs = args.subList(1, size); + + return parseCommandArgs(conf, coreArgs); + } + + /** + * Parse the command arguments, extracting the service class as the last + * element of the list (after extracting all the rest). + * + * The field {@link #commandOptions} field must already have been set. + * @param conf configuration to use + * @param args command line argument list + * @return the remaining arguments + * @throws ServiceLaunchException if processing of arguments failed + */ + protected List<String> parseCommandArgs(Configuration conf, + List<String> args) { + Preconditions.checkNotNull(commandOptions, + "Command options have not been created"); + StringBuilder argString = new StringBuilder(args.size() * 32); + for (String arg : args) { + argString.append("\"").append(arg).append("\" "); + } + LOG.debug("Command line: {}", argString); + try { + String[] argArray = args.toArray(new String[args.size()]); + // parse this the standard way. This will + // update the configuration in the parser, and potentially + // patch the user credentials + GenericOptionsParser parser = createGenericOptionsParser(conf, argArray); + if (!parser.isParseSuccessful()) { + throw new ServiceLaunchException(EXIT_COMMAND_ARGUMENT_ERROR, + E_PARSE_FAILED + " %s", argString); + } + CommandLine line = parser.getCommandLine(); + List<String> remainingArgs = Arrays.asList(parser.getRemainingArgs()); + LOG.debug("Remaining arguments {}", remainingArgs); + + // Scan the list of configuration files + // and bail out if they don't exist + if (line.hasOption(ARG_CONF)) { + String[] filenames = line.getOptionValues(ARG_CONF); + verifyConfigurationFilesExist(filenames); + // Add URLs of files as list of URLs to load + for (String filename : filenames) { + File file = new File(filename); + LOG.debug("Configuration files {}", file); + confResourceUrls.add(file.toURI().toURL()); + } + } + if (line.hasOption(ARG_CONFCLASS)) { + // new resources to instantiate as configurations + List<String> classnameList = Arrays.asList( + line.getOptionValues(ARG_CONFCLASS)); + LOG.debug("Configuration classes {}", classnameList); + confClassnames.addAll(classnameList); + } + // return the remainder + return remainingArgs; + } catch (IOException e) { + // parsing problem: convert to a command argument error with + // the original text + throw new ServiceLaunchException(EXIT_COMMAND_ARGUMENT_ERROR, e); + } catch (RuntimeException e) { + // lower level issue such as XML parse failure + throw new ServiceLaunchException(EXIT_COMMAND_ARGUMENT_ERROR, + E_PARSE_FAILED + " %s : %s", argString, e); + } + } + + /** + * Override point: create a generic options parser or subclass thereof. + * @param conf Hadoop configuration + * @param argArray array of arguments + * @return a generic options parser to parse the arguments + * @throws IOException on any failure + */ + protected GenericOptionsParser createGenericOptionsParser(Configuration conf, + String[] argArray) throws IOException { + return new MinimalGenericOptionsParser(conf, commandOptions, argArray); + } + + /** + * Verify that all the specified filenames exist. + * @param filenames a list of files + * @throws ServiceLaunchException if a file is not found + */ + protected void verifyConfigurationFilesExist(String[] filenames) { + if (filenames == null) { + return; + } + for (String filename : filenames) { + File file = new File(filename); + LOG.debug("Conf file {}", file.getAbsolutePath()); + if (!file.exists()) { + // no configuration file + throw new ServiceLaunchException(EXIT_NOT_FOUND, + ARG_CONF_PREFIXED + ": configuration file not found: %s", + file.getAbsolutePath()); + } + } + } + + /** + * Build a log message for starting up and shutting down. + * @param classname the class of the server + * @param args arguments + */ + protected static String startupShutdownMessage(String classname, + List<String> args) { + final String hostname = NetUtils.getHostname(); + + return StringUtils.createStartupShutdownMessage(classname, hostname, + args.toArray(new String[args.size()])); + } + + /** + * Exit with a printed message. + * @param status status code + * @param message message message to print before exiting + * @throws ExitUtil.ExitException if exceptions are disabled + */ + protected static void exitWithMessage(int status, String message) { + ExitUtil.terminate(new ServiceLaunchException(status, message)); + } + + /** + * Exit with the usage exit code {@link #EXIT_USAGE} + * and message {@link #USAGE_MESSAGE}. + * @throws ExitUtil.ExitException if exceptions are disabled + */ + protected static void exitWithUsageMessage() { + exitWithMessage(EXIT_USAGE, USAGE_MESSAGE); + } + + /** + * This is the JVM entry point for the service launcher. + * + * Converts the arguments to a list, then invokes {@link #serviceMain(List)} + * @param args command line arguments. + */ + public static void main(String[] args) { + serviceMain(Arrays.asList(args)); + } + + /** + * Varargs version of the entry point for testing and other in-JVM use. + * Hands off to {@link #serviceMain(List)} + * @param args command line arguments. + */ + public static void serviceMain(String... args) { + serviceMain(Arrays.asList(args)); + } + + /* ====================================================================== */ + /** + * The real main function, which takes the arguments as a list. + * Argument 0 MUST be the service classname + * @param argsList the list of arguments + */ + /* ====================================================================== */ + + public static void serviceMain(List<String> argsList) { + if (argsList.isEmpty()) { + // no arguments: usage message + exitWithUsageMessage(); + } else { + ServiceLauncher<Service> serviceLauncher = + new ServiceLauncher<>(argsList.get(0)); + serviceLauncher.launchServiceAndExit(argsList); + } + } + + /** + * A generic options parser which does not parse any of the traditional + * Hadoop options. + */ + protected static class MinimalGenericOptionsParser + extends GenericOptionsParser { + public MinimalGenericOptionsParser(Configuration conf, + Options options, String[] args) throws IOException { + super(conf, options, args); + } + + @Override + protected Options buildGeneralOptions(Options opts) { + return opts; + } + } +} http://git-wip-us.apache.org/repos/asf/hadoop/blob/373bb493/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceShutdownHook.java ---------------------------------------------------------------------- diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceShutdownHook.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceShutdownHook.java new file mode 100644 index 0000000..9115f4e --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceShutdownHook.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.service.launcher; + +import java.lang.ref.WeakReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.service.Service; +import org.apache.hadoop.util.ShutdownHookManager; + +/** + * JVM Shutdown hook for Service which will stop the + * Service gracefully in case of JVM shutdown. + * This hook uses a weak reference to the service, + * and when shut down, calls {@link Service#stop()} if the reference is valid. + */ [email protected] [email protected] +public class ServiceShutdownHook implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger( + ServiceShutdownHook.class); + + /** + * A weak reference to the service. + */ + private final WeakReference<Service> serviceRef; + + /** + * Create an instance. + * @param service the service + */ + public ServiceShutdownHook(Service service) { + serviceRef = new WeakReference<>(service); + } + + /** + * Register the service for shutdown with Hadoop's + * {@link ShutdownHookManager}. + * @param priority shutdown hook priority + */ + public synchronized void register(int priority) { + unregister(); + ShutdownHookManager.get().addShutdownHook(this, priority); + } + + /** + * Unregister the hook. + */ + public synchronized void unregister() { + try { + ShutdownHookManager.get().removeShutdownHook(this); + } catch (IllegalStateException e) { + LOG.info("Failed to unregister shutdown hook: {}", e, e); + } + } + + /** + * Shutdown handler. + * Query the service hook reference -if it is still valid the + * {@link Service#stop()} operation is invoked. + */ + @Override + public void run() { + shutdown(); + } + + /** + * Shutdown operation. + * <p> + * Subclasses may extend it, but it is primarily + * made available for testing. + * @return true if the service was stopped and no exception was raised. + */ + protected boolean shutdown() { + Service service; + boolean result = false; + synchronized (this) { + service = serviceRef.get(); + serviceRef.clear(); + } + if (service != null) { + try { + // Stop the Service + service.stop(); + result = true; + } catch (Throwable t) { + LOG.info("Error stopping {}", service.getName(), t); + } + } + return result; + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
