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 0ac1821554..ba4e0d269b 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 @@ 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 static RuntimeException convert(Throwable fault) { * {@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) { 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 0000000000..be28c5be2d --- /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. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +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} + *
+ * The base implementation logs all arguments at the debug level,
+ * then returns the passed in config unchanged.
+ */
+
+ @Override
+ public Configuration bindArgs(Configuration config, List
+ * The action is to signal success by returning the exit code 0.
+ */
+ @Override
+ public int execute() throws Exception {
+ return LauncherExitCodes.EXIT_SUCCESS;
+ }
+}
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 0000000000..bf4a86360a
--- /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);
+ }
+ }
+
+ }
+
+}
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 0000000000..a7e1edd007
--- /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.
+ *
+ * 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.
+ */
+@InterfaceAudience.Public
+@InterfaceStability.Evolving
+public interface LaunchableService extends Service {
+
+ /**
+ * Propagate the command line arguments.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @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
+ * The return value becomes the exit code of the launched process.
+ *
+ * If an exception is raised, the policy is:
+ *
+ * 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: ";
+}
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 0000000000..f48e38e5f2
--- /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.
+ *
+ * Codes with a YARN prefix are YARN-related.
+ *
+ * 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.
+ *
+ * Approximate HTTP equivalent: {@code 400 Bad Request}
+ */
+ int EXIT_COMMAND_ARGUMENT_ERROR = 40;
+
+ /**
+ * The request requires user authentication: {@value}.
+ *
+ * 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}.
+ *
+ * Approximate HTTP equivalent: Approximate HTTP equivalent: {@code 403: Forbidden}
+ */
+ int EXIT_FORBIDDEN = 43;
+
+ /**
+ * Something was not found: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 404: Not Found}
+ */
+ int EXIT_NOT_FOUND = 44;
+
+ /**
+ * The operation is not allowed: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 405: Not allowed}
+ */
+ int EXIT_OPERATION_NOT_ALLOWED = 45;
+
+ /**
+ * The command is somehow not acceptable: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 406: Not Acceptable}
+ */
+ int EXIT_NOT_ACCEPTABLE = 46;
+
+ /**
+ * Exit code on connectivity problems: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 408: Request Timeout}
+ */
+ int EXIT_CONNECTIVITY_PROBLEM = 48;
+
+ /**
+ * Exit code when the configurations in valid/incomplete: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 409: Conflict}
+ */
+ int EXIT_BAD_CONFIGURATION = 49;
+
+ /**
+ * Exit code when an exception was thrown from the service: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 500 Internal Server Error}
+ */
+ int EXIT_EXCEPTION_THROWN = 50;
+
+ /**
+ * Unimplemented feature: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 501: Not Implemented}
+ */
+ int EXIT_UNIMPLEMENTED = 51;
+
+ /**
+ * Service Unavailable; it may be available later: {@value}.
+ *
+ * Approximate HTTP equivalent: {@code 503 Service Unavailable}
+ */
+ int EXIT_SERVICE_UNAVAILABLE = 53;
+
+ /**
+ * The application does not support, or refuses to support this
+ * version: {@value}.
+ *
+ * If raised, this is expected to be raised server-side and likely due
+ * to client/server version incompatibilities.
+ *
+ * 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;
+
+}
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 0000000000..1243a1fabf
--- /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.
+ *
+ * When caught by the ServiceLauncher, it will convert that
+ * into a process exit code.
+ *
+ * The {@link #ServiceLaunchException(int, String, Object...)} constructor
+ * generates formatted exceptions.
+ */
+@InterfaceAudience.Public
+@InterfaceStability.Evolving
+
+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.
+ *
+ * This uses {@link String#format(String, Object...)}
+ * to build the formatted exception in the ENGLISH locale.
+ *
+ * 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]);
+ }
+ }
+
+}
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 0000000000..6b0b4e8628
--- /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:
+ *
+ * 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.
+ *
+ * This tries to log to {@code LOG.error()}.
+ *
+ * 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 may 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
+ * 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;
+ }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/package-info.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/package-info.java
new file mode 100644
index 0000000000..85163575d7
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/package-info.java
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+
+/**
+
+ This package contains classes, interfaces and exceptions to launch
+ YARN services from the command line.
+
+
+ General purpose YARN service launcher:
+ The {@link org.apache.hadoop.service.launcher.ServiceLauncher} class parses
+ a command line, then instantiates and launches the specified YARN service. It
+ then waits for the service to finish, converting any exceptions raised or
+ exit codes returned into an exit code for the (then exited) process.
+
+ This class is designed be invokable from the static
+ {@link org.apache.hadoop.service.launcher.ServiceLauncher#main(String[])}
+ method, or from {@code main(String[])} methods implemented by
+ other classes which provide their own entry points.
+
+
+
+ Extended YARN Service Interface:
+ The {@link org.apache.hadoop.service.launcher.LaunchableService} interface
+ extends {@link org.apache.hadoop.service.Service} with methods to pass
+ down the CLI arguments and to execute an operation without having to
+ spawn a thread in the {@link org.apache.hadoop.service.Service#start()} phase.
+
+
+
+ Standard Exit codes:
+ {@link org.apache.hadoop.service.launcher.LauncherExitCodes}
+ defines a set of exit codes that can be used by services to standardize
+ exit causes.
+
+
+ Escalated shutdown:
+ The {@link org.apache.hadoop.service.launcher.ServiceShutdownHook}
+ shuts down any service via the hadoop shutdown mechanism.
+ The {@link org.apache.hadoop.service.launcher.InterruptEscalator} can be
+ registered to catch interrupts, triggering the shutdown -and forcing a JVM
+ exit if it times out or a second interrupt is received.
+
+ Tests: test cases include interrupt handling and
+ lifecycle failures.
+
+
+
+ The launcher will initialize the service via
+ {@link org.apache.hadoop.service.Service#init(Configuration)},
+ then start it via its {@link org.apache.hadoop.service.Service#start()} method.
+ It then waits indefinitely for the service to stop.
+
+ After the service has stopped, a non-null value of
+ {@link org.apache.hadoop.service.Service#getFailureCause()} is interpreted
+ as a failure, and, if it didn't happen during the stop phase (i.e. when
+ {@link org.apache.hadoop.service.Service#getFailureState()} is not
+ {@code STATE.STOPPED}, escalated into a non-zero return code).
+
+
+ To view the workflow in sequence, it is:
+
+ To summarize: provided a service launches its long-lived threads in its Service
+ {@code start()} method, the service launcher can create it, configure it
+ and start it, triggering shutdown when signaled.
+
+ What these services can not do is get at the command line parameters or easily
+ propagate exit codes (there is a way covered later). These features require
+ some extensions to the base {@code Service} interface: the Launchable
+ Service.
+
+
+ It adds two methods to the service interface —and hence two new features:
+
+
+ It can just as easily be used for implementing long-lived services that
+ parse the command line -it just becomes the responsibility of the
+ service to decide when to return from the {@code execute()} method.
+ It doesn't even need to {@code stop()} itself; the launcher will handle
+ that if necessary.
+
+ The {@link org.apache.hadoop.service.launcher.LaunchableService} interface
+ extends {@link org.apache.hadoop.service.Service} with two new methods.
+
+
+ {@link org.apache.hadoop.service.launcher.LaunchableService#bindArgs(Configuration, List)}
+ provides the {@code main(String args[])} arguments as a list, after any
+ processing by the Service Launcher to extract configuration file references.
+ This method is called before
+ {@link org.apache.hadoop.service.Service#init(Configuration)}.
+ This is by design: it allows the arguments to be parsed before the service is
+ initialized, thus allowing services to tune their configuration data before
+ passing it to any superclass in that {@code init()} method.
+ To make this operation even simpler, the
+ {@link org.apache.hadoop.conf.Configuration} that is to be passed in
+ is provided as an argument.
+ This reference passed in is the initial configuration for this service;
+ the one that will be passed to the init operation.
+
+ In
+ {@link org.apache.hadoop.service.launcher.LaunchableService#bindArgs(Configuration, List)},
+ a Launchable Service may manipulate this configuration by setting or removing
+ properties. It may also create a new {@code Configuration} instance
+ which may be needed to trigger the injection of HDFS or YARN resources
+ into the default resources of all Configurations.
+ If the return value of the method call is a configuration
+ reference (as opposed to a null value), the returned value becomes that
+ passed in to the {@code init()} method.
+
+ After the {@code bindArgs()} processing, the service's {@code init()}
+ and {@code start()} methods are called, as usual.
+
+ At this point, rather than block waiting for the service to terminate (as
+ is done for a basic service), the method
+ {@link org.apache.hadoop.service.launcher.LaunchableService#execute()}
+ is called.
+ This is a method expected to block until completed, returning the intended
+ application exit code of the process when it does so.
+
+ After this {@code execute()} operation completes, the
+ service is stopped and exit codes generated. Any exception raised
+ during the {@code execute()} method takes priority over any exit codes
+ returned by the method. This allows services to signal failures simply
+ by raising exceptions with exit codes.
+
+
+
+ To view the workflow in sequence, it is:
+
+ For a basic service, the return code is 0 unless an exception
+ was raised.
+
+ For a {@link org.apache.hadoop.service.launcher.LaunchableService}, the return
+ code is the number returned from the
+ {@link org.apache.hadoop.service.launcher.LaunchableService#execute()}
+ operation, again, unless overridden an exception was raised.
+
+
+ Exceptions are converted into exit codes -but rather than simply
+ have a "something went wrong" exit code, exceptions may
+ provide exit codes which will be extracted and used as the return code.
+ This enables Launchable Services to use exceptions as a way
+ of returning error codes to signal failures and for
+ normal Services to return any error code at all.
+
+
+ Any exception which implements the
+ {@link org.apache.hadoop.util.ExitCodeProvider}
+ interface is considered be a provider of the exit code: the method
+ {@link org.apache.hadoop.util.ExitCodeProvider#getExitCode()}
+ will be called on the caught exception to generate the return code.
+ This return code and the message in the exception will be used to
+ generate an instance of
+ {@link org.apache.hadoop.util.ExitUtil.ExitException}
+ which can be passed down to
+ {@link org.apache.hadoop.util.ExitUtil#terminate(ExitUtil.ExitException)}
+ to trigger a JVM exit. The initial exception will be used as the cause
+ of the {@link org.apache.hadoop.util.ExitUtil.ExitException}.
+
+
+ If the exception is already an instance or subclass of
+ {@link org.apache.hadoop.util.ExitUtil.ExitException}, it is passed
+ directly to
+ {@link org.apache.hadoop.util.ExitUtil#terminate(ExitUtil.ExitException)}
+ without any conversion.
+ One such subclass,
+ {@link org.apache.hadoop.service.launcher.ServiceLaunchException}
+ may be useful: it includes formatted exception message generation.
+ It also declares that it extends the
+ {@link org.apache.hadoop.service.launcher.LauncherExitCodes}
+ interface listing common exception codes. These are exception codes
+ that can be raised by the {@link org.apache.hadoop.service.launcher.ServiceLauncher}
+ itself to indicate problems during parsing the command line, creating
+ the service instance and the like. There are also some common exit codes
+ for Hadoop/YARN service failures, such as
+ {@link org.apache.hadoop.service.launcher.LauncherExitCodes#EXIT_UNAUTHORIZED}.
+ Note that {@link org.apache.hadoop.util.ExitUtil.ExitException} itself
+ implements {@link org.apache.hadoop.util.ExitCodeProvider#getExitCode()}
+
+
+ If an exception does not implement
+ {@link org.apache.hadoop.util.ExitCodeProvider#getExitCode()},
+ it will be wrapped in an {@link org.apache.hadoop.util.ExitUtil.ExitException}
+ with the exit code
+ {@link org.apache.hadoop.service.launcher.LauncherExitCodes#EXIT_EXCEPTION_THROWN}.
+
+
+ To view the exit code extraction in sequence, it is:
+
+ If a second signal is received, the
+ {@link org.apache.hadoop.service.launcher.InterruptEscalator}
+ reacts by triggering an immediate JVM halt, invoking
+ {@link org.apache.hadoop.util.ExitUtil#halt(int, String)}.
+ This escalation process is designed to address the situation in which
+ a shutdown-hook can block, yet the caller (such as an init.d daemon)
+ wishes to kill the process.
+ The shutdown script should repeat the kill signal after a chosen time period,
+ to trigger the more aggressive process halt. The exit code will always be
+ {@link org.apache.hadoop.service.launcher.LauncherExitCodes#EXIT_INTERRUPTED}.
+
+ The {@link org.apache.hadoop.service.launcher.ServiceLauncher} also registers
+ a {@link org.apache.hadoop.service.launcher.ServiceShutdownHook} with the
+ Hadoop shutdown hook manager, unregistering it afterwards. This hook will
+ stop the service if a shutdown request is received, so ensuring that
+ if the JVM is exited by any thread, an attempt to shut down the service
+ will be made.
+
+
+
+ What the launcher does do is use reflection to try and create instances of
+ these classes simply to force in the common resources. If the classes are
+ not on the classpath this fact will be logged.
+
+ Applications may consider it essential to either force load in the relevant
+ configuration, or pass it down to the service being created. In which
+ case further measures may be needed.
+
+ 1: Creation in an extended {@code ServiceLauncher}
+
+
+ Subclass the Service launcher and override its
+ {@link org.apache.hadoop.service.launcher.ServiceLauncher#createConfiguration()}
+ method with one that creates the right configuration.
+ This is good if a single
+ launcher can be created for all services launched by a module, such as
+ HDFS or YARN. It does imply a dedicated script to invoke the custom
+ {@code main()} method.
+
+ 2: Creation in {@code bindArgs()}
+
+
+ In
+ {@link org.apache.hadoop.service.launcher.LaunchableService#bindArgs(Configuration, List)},
+ a new configuration is created:
+
+ 3: Creation in {@code serviceInit()}
+
+
+ This is a strategy used by many existing YARN services, and is ideal for
+ services which do not implement the LaunchableService interface. Its one
+ weakness is that the configuration is now private to that instance. Some
+ YARN services use a single shared configuration instance as a way of
+ propagating information between peer services in a
+ {@link org.apache.hadoop.service.CompositeService}.
+ While a dangerous practice, it does happen.
+
+
+ Summary: the ServiceLauncher makes a best-effort attempt to load the
+ standard Configuration subclasses, but does not fail if they are not present.
+ Services which require a specific subclasses should follow one of the
+ strategies listed;
+ creation in {@code serviceInit()} is the recommended policy.
+
+
+ It will be loaded into the Hadoop configuration
+ class (the one created by the
+ {@link org.apache.hadoop.service.launcher.ServiceLauncher#createConfiguration()}
+ method.
+ If this argument is repeated multiple times, all configuration
+ files are merged with the latest file on the command line being the
+ last one to be applied.
+
+ All the {@code --conf <file>} argument pairs are stripped off
+ the argument list provided to the instantiated service; they get the
+ merged configuration, but not the commands used to create it.
+
+
+ * @param status exit code to use if the exception is not an ExitException.
+ * @param t throwable which triggered the termination. If this exception
+ * is an {@link ExitException} its status overrides that passed in.
+ * @throws ExitException if {@link System#exit(int)} is disabled.
*/
public static void terminate(int status, Throwable t) throws ExitException {
- terminate(status, StringUtils.stringifyException(t));
+ if (t instanceof ExitException) {
+ terminate((ExitException) t);
+ } else {
+ terminate(new ExitException(status, t));
+ }
}
/**
* Forcibly terminates the currently running Java virtual machine.
*
- * @param status
- * @param t
- * @throws ExitException
+ * @param status exit code to use if the exception is not a HaltException.
+ * @param t throwable which triggered the termination. If this exception
+ * is a {@link HaltException} its status overrides that passed in.
+ * @throws HaltException if {@link System#exit(int)} is disabled.
*/
public static void halt(int status, Throwable t) throws HaltException {
- halt(status, StringUtils.stringifyException(t));
+ if (t instanceof HaltException) {
+ halt((HaltException) t);
+ } else {
+ halt(new HaltException(status, t));
+ }
}
/**
- * Like {@link terminate(int, String)} without a message.
+ * Like {@link #terminate(int, Throwable)} without a message.
*
- * @param status
- * @throws ExitException
- * if System.exit is disabled for test purposes
+ * @param status exit code
+ * @throws ExitException if {@link System#exit(int)} is disabled.
*/
public static void terminate(int status) throws ExitException {
- terminate(status, "ExitException");
+ terminate(status, "");
+ }
+
+ /**
+ * Terminate the current process. Note that terminate is the *only* method
+ * that should be used to terminate the daemon processes.
+ *
+ * @param status exit code
+ * @param msg message used to create the {@code ExitException}
+ * @throws ExitException if {@link System#exit(int)} is disabled.
+ */
+ public static void terminate(int status, String msg) throws ExitException {
+ terminate(new ExitException(status, msg));
}
/**
* Forcibly terminates the currently running Java virtual machine.
- * @param status
- * @throws ExitException
+ * @param status status code
+ * @throws HaltException if {@link Runtime#halt(int)} is disabled.
*/
public static void halt(int status) throws HaltException {
- halt(status, "HaltException");
+ halt(status, "");
+ }
+
+ /**
+ * Forcibly terminates the currently running Java virtual machine.
+ * @param status status code
+ * @param message message
+ * @throws HaltException if {@link Runtime#halt(int)} is disabled.
+ */
+ public static void halt(int status, String message) throws HaltException {
+ halt(new HaltException(status, message));
+ }
+
+ /**
+ * Handler for out of memory events -no attempt is made here
+ * to cleanly shutdown or support halt blocking; a robust
+ * printing of the event to stderr is all that can be done.
+ * @param oome out of memory event
+ */
+ public static void haltOnOutOfMemory(OutOfMemoryError oome) {
+ //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 done want to exit because of logging issues.
+ }
+ Runtime.getRuntime().halt(-1);
}
}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/GenericOptionsParser.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/GenericOptionsParser.java
index cd1fc831b3..835206a89f 100644
--- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/GenericOptionsParser.java
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/GenericOptionsParser.java
@@ -15,9 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.hadoop.util;
-
-import java.io.File;
+package org.apache.hadoop.util;import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
@@ -118,6 +116,7 @@ public class GenericOptionsParser {
private static final Log LOG = LogFactory.getLog(GenericOptionsParser.class);
private Configuration conf;
private CommandLine commandLine;
+ private final boolean parseSuccessful;
/**
* Create an options parser with the given options to parse the args.
@@ -171,7 +170,7 @@ public GenericOptionsParser(Configuration conf, String[] args)
public GenericOptionsParser(Configuration conf,
Options options, String[] args) throws IOException {
this.conf = conf;
- parseGeneralOptions(options, args);
+ parseSuccessful = parseGeneralOptions(options, args);
}
/**
@@ -208,58 +207,72 @@ public CommandLine getCommandLine() {
}
/**
- * Specify properties of each generic option
+ * Query for the parse operation succeeding.
+ * @return true if parsing the CLI was successful
+ */
+ public boolean isParseSuccessful() {
+ return parseSuccessful;
+ }
+
+ /**
+ * Specify properties of each generic option.
+ * Important clazz, String[] args,
final LogAdapter LOG) {
final String hostname = NetUtils.getHostname();
final String classname = clazz.getSimpleName();
- LOG.info(
- toStartupShutdownString("STARTUP_MSG: ", new String[] {
- "Starting " + classname,
- " user = " + System.getProperty("user.name"),
- " host = " + hostname,
- " args = " + Arrays.asList(args),
- " version = " + VersionInfo.getVersion(),
- " classpath = " + System.getProperty("java.class.path"),
- " build = " + VersionInfo.getUrl() + " -r "
- + VersionInfo.getRevision()
- + "; compiled by '" + VersionInfo.getUser()
- + "' on " + VersionInfo.getDate(),
- " java = " + System.getProperty("java.version") }
- )
- );
+ LOG.info(createStartupShutdownMessage(classname, hostname, args));
if (SystemUtils.IS_OS_UNIX) {
try {
@@ -745,6 +731,29 @@ public void run() {
}
+ /**
+ * Generate the text for the startup/shutdown message of processes.
+ * @param classname short classname of the class
+ * @param hostname hostname
+ * @param args Command arguments
+ * @return a string to log.
+ */
+ public static String createStartupShutdownMessage(String classname,
+ String hostname, String[] args) {
+ return toStartupShutdownString("STARTUP_MSG: ", new String[] {
+ "Starting " + classname,
+ " host = " + hostname,
+ " args = " + Arrays.asList(args),
+ " version = " + VersionInfo.getVersion(),
+ " classpath = " + System.getProperty("java.class.path"),
+ " build = " + VersionInfo.getUrl() + " -r "
+ + VersionInfo.getRevision()
+ + "; compiled by '" + VersionInfo.getUser()
+ + "' on " + VersionInfo.getDate(),
+ " java = " + System.getProperty("java.version") }
+ );
+ }
+
/**
* The traditional binary prefixes, kilo, mega, ..., exa,
* which can be represented by a 64-bit integer.
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/BreakableService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/BreakableService.java
index eeb1a3192e..d84447ba8b 100644
--- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/BreakableService.java
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/BreakableService.java
@@ -20,8 +20,6 @@
package org.apache.hadoop.service;
import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.service.AbstractService;
-import org.apache.hadoop.service.Service;
/**
* This is a service that can be configured to break on any of the lifecycle
@@ -69,12 +67,21 @@ public int getCount(STATE state) {
return counts[convert(state)];
}
- private void maybeFail(boolean fail, String action) {
+ private void maybeFail(boolean fail, String action) throws Exception {
if (fail) {
- throw new BrokenLifecycleEvent(this, action);
+ throw createFailureException(action);
}
}
+ /**
+ * Override point: create the exception to raise
+ * @param action action in progress
+ * @return the exception that will be thrown
+ */
+ protected Exception createFailureException(String action) {
+ return new BrokenLifecycleEvent(this, action);
+ }
+
@Override
protected void serviceInit(Configuration conf) throws Exception {
inc(STATE.INITED);
@@ -83,13 +90,13 @@ protected void serviceInit(Configuration conf) throws Exception {
}
@Override
- protected void serviceStart() {
+ protected void serviceStart() throws Exception {
inc(STATE.STARTED);
maybeFail(failOnStart, "start");
}
@Override
- protected void serviceStop() {
+ protected void serviceStop() throws Exception {
inc(STATE.STOPPED);
maybeFail(failOnStop, "stop");
}
@@ -107,11 +114,11 @@ public void setFailOnStop(boolean failOnStop) {
}
/**
- * The exception explicitly raised on a failure
+ * The exception explicitly raised on a failure.
*/
public static class BrokenLifecycleEvent extends RuntimeException {
- final STATE state;
+ public final STATE state;
public BrokenLifecycleEvent(Service service, String action) {
super("Lifecycle Failure during " + action + " state is "
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/AbstractServiceLauncherTestBase.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/AbstractServiceLauncherTestBase.java
new file mode 100644
index 0000000000..127b0b3827
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/AbstractServiceLauncherTestBase.java
@@ -0,0 +1,317 @@
+/*
+ * 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.commons.lang.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.service.Service;
+import org.apache.hadoop.service.ServiceOperations;
+import static org.apache.hadoop.test.GenericTestUtils.*;
+import org.apache.hadoop.util.ExitCodeProvider;
+import org.apache.hadoop.util.ExitUtil;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+import org.junit.rules.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+
+public class AbstractServiceLauncherTestBase extends Assert implements
+ LauncherExitCodes {
+ private static final Logger LOG = LoggerFactory.getLogger(
+ AbstractServiceLauncherTestBase.class);
+ public static final String CONF_FILE_DIR = "target/launcher/conf";
+
+ /**
+ * A service which will be automatically stopped on teardown.
+ */
+ private Service serviceToTeardown;
+
+ /**
+ * All tests have a short life.
+ */
+ @Rule
+ public Timeout testTimeout = new Timeout(15000);
+
+ /**
+ * Rule to provide the method name.
+ */
+ @Rule
+ public TestName methodName = new TestName();
+
+ /**
+ * Turn off the exit util JVM exits, downgrading them to exception throws.
+ */
+ @BeforeClass
+ public static void disableJVMExits() {
+ ExitUtil.disableSystemExit();
+ ExitUtil.disableSystemHalt();
+ }
+
+ /**
+ * rule to name the thread JUnit.
+ */
+ @Before
+ public void nameThread() {
+ Thread.currentThread().setName("JUnit");
+ }
+
+ @After
+ public void stopService() {
+ ServiceOperations.stopQuietly(serviceToTeardown);
+ }
+
+ public void setServiceToTeardown(Service serviceToTeardown) {
+ this.serviceToTeardown = serviceToTeardown;
+ }
+
+ /**
+ * Assert that a service is in a state.
+ * @param service service
+ * @param expected expected state
+ */
+ protected void assertInState(Service service, Service.STATE expected) {
+ assertNotNull(service);
+ Service.STATE actual = service.getServiceState();
+ failif(actual != expected,
+ "Service %s in state %s expected state: %s", service.getName(), actual, expected);
+
+ }
+
+ /**
+ * Assert a service has stopped.
+ * @param service service
+ */
+ protected void assertStopped(Service service) {
+ assertInState(service, Service.STATE.STOPPED);
+ }
+
+ /**
+ * Assert that an exception code matches the value expected.
+ * @param expected expected value
+ * @param text text in exception -can be null
+ * @param e exception providing the actual value
+ */
+ protected void assertExceptionDetails(int expected,
+ String text,
+ ExitCodeProvider e) {
+ assertNotNull(e);
+ String toString = e.toString();
+ int exitCode = e.getExitCode();
+ boolean failed = expected != exitCode;
+ failed |= StringUtils.isNotEmpty(text)
+ && !StringUtils.contains(toString, text);
+ failif(failed,
+ "Expected exception with exit code %d and text \"%s\""
+ + " but got the exit code %d"
+ + " in \"%s\"",
+ expected, text,
+ exitCode, e);
+ }
+
+ /**
+ * Assert the launch come was a service creation failure.
+ * @param classname argument
+ */
+ protected void assertServiceCreationFails(String classname) {
+ assertLaunchOutcome(EXIT_SERVICE_CREATION_FAILURE, "", classname);
+ }
+
+ /**
+ * Assert a launch outcome.
+ * @param expected expected value
+ * @param text text in exception -can be null
+ * @param args CLI args
+ */
+ protected void assertLaunchOutcome(int expected,
+ String text,
+ String... args) {
+ try {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Launching service with expected outcome {}", expected);
+ for (String arg : args) {
+ LOG.debug(arg);
+ }
+ }
+ ServiceLauncher.serviceMain(args);
+ } catch (ServiceLaunchException e) {
+ assertExceptionDetails(expected, text, e);
+ }
+ }
+
+ /**
+ * Assert a launch runs.
+ * @param args CLI args
+ */
+ protected void assertRuns(String... args) {
+ assertLaunchOutcome(0, "", args);
+ }
+
+ /**
+ * Init and start a service.
+ * @param service the service
+ * @return the service
+ */
+ protected
+ *
+ *
+ */
+
+@InterfaceAudience.Private
+@InterfaceStability.Unstable
+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 WeakReferencekill
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 + ')';
+ }
+ }
+}
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 0000000000..fb0a052211
--- /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.
+ *
+ *
+ * @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;
+}
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 0000000000..08118f59db
--- /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.
+ *
+ * 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
+ *
+ */
+@InterfaceAudience.Public
+@InterfaceStability.Evolving
+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.
+ *
+ *
+ * Error and warning messages are logged to {@code stderr}.
+ *
+ * @param service class to cast the generated service to.
+ */
+@SuppressWarnings("UseOfSystemOutOrSystemErr")
+public class ServiceLauncher
+ 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
+ *
+ * @param args arguments to the service. {@code arg[0]} is
+ * assumed to be the service classname.
+ */
+ public void launchServiceAndExit(List
+ *
+ *
+ * @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:
+ *
+ * Thread.setDefaultUncaughtExceptionHandler(
+ * new YarnUncaughtExceptionHandler());
+ *
+ */
+ 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.
+ * Key Features
+
+ Launching a YARN Service
+
+ The Service Launcher can launch any YARN service.
+ It will instantiate the service classname provided, using the no-args
+ constructor, or if no such constructor is available, it will fall back
+ to a constructor with a single {@code String} parameter,
+ passing the service name as the parameter value.
+
+
+
+ For a service to be fully compatible with this launch model, it must
+
+
+
+ If a service does not stop itself, ever, then it can be launched
+ as a long-lived daemon.
+ The service launcher will never terminate, but neither will the service.
+ The service launcher does register signal handlers to catch {@code kill}
+ and control-C signals —calling {@code stop()} on the service when
+ signaled.
+ This means that a daemon service may get a warning and time to shut
+ down.
+
+ Launching a Launchable YARN Service
+
+ A Launchable YARN Service is a YARN service which implements the interface
+ {@link org.apache.hadoop.service.launcher.LaunchableService}.
+
+
+
+ This design is ideal for implementing services which parse the command line,
+ and which execute short-lived applications. For example, end user
+ commands can be implemented as such services, thus integrating with YARN's
+ workflow and {@code YarnClient} client-side code.
+
+
+
+
+
+ Exit Codes and Exceptions
+
+
+
+
+ This process may seem convoluted, but it is designed to allow any exception
+ in the Hadoop exception hierarchy to generate exit codes,
+ and to minimize the amount of exception wrapping which takes place.
+
+ Interrupt Handling
+
+ The Service Launcher has a helper class,
+ {@link org.apache.hadoop.service.launcher.InterruptEscalator}
+ to handle the standard SIGKILL signal and control-C signals.
+ This class registers for signal callbacks from these signals, and,
+ when received, attempts to stop the service in a limited period of time.
+ It then triggers a JVM shutdown by way of
+ {@link org.apache.hadoop.util.ExitUtil#terminate(int, String)}
+ Configuration class creation
+
+ The Configuration class used to initialize a service is a basic
+ {@link org.apache.hadoop.conf.Configuration} instance. As the launcher is
+ the entry point for an application, this implies that the HDFS, YARN or other
+ default configurations will not have been forced in through the constructors
+ of {@code HdfsConfiguration} or {@code YarnConfiguration}.
+
+ public Configuration bindArgs(Configuration config, List
+
+ This guarantees a configuration of the right type is generated for all
+ instances created via the service launcher. It does imply that this is
+ expected to be only way that services will be launched.
+
+
+ protected void serviceInit(Configuration conf) throws Exception {
+ super.serviceInit(new YarnConfiguration(conf));
+ }
+
+
+ Configuration file loading
+
+ Before the service is bound to the CLI, the ServiceLauncher scans through
+ all the arguments after the first one, looking for instances of the argument
+ {@link org.apache.hadoop.service.launcher.ServiceLauncher#ARG_CONF}
+ argument pair: {@code --conf <file>}. This must refer to a file
+ in the local filesystem which exists.
+ Utility Classes
+
+
+
+
+ */
+
+
+package org.apache.hadoop.service.launcher;
+
+import java.util.List;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.util.ExitUtil;
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitCodeProvider.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitCodeProvider.java
new file mode 100644
index 0000000000..0424ba0f25
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitCodeProvider.java
@@ -0,0 +1,35 @@
+/*
+ * 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.util;
+
+/**
+ * Get the exit code of an exception.
+ * Making it an interface makes
+ * it possible to retrofit exit codes onto existing classes,
+ * and add exit code providers under all parts of the Exception tree.
+ */
+
+public interface ExitCodeProvider {
+
+ /**
+ * Method to get the exit code.
+ * @return the exit code
+ */
+ int getExitCode();
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitUtil.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitUtil.java
index 52089274a1..5642a2340d 100644
--- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitUtil.java
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitUtil.java
@@ -17,41 +17,123 @@
*/
package org.apache.hadoop.util;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
- * Facilitates hooking process termination for tests and debugging.
+ * Facilitates hooking process termination for tests, debugging
+ * and embedding.
+ *
+ * Hadoop code that attempts to call {@link System#exit(int)}
+ * or {@link Runtime#halt(int)} MUST invoke it via these methods.
*/
-@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
+@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce", "YARN"})
@InterfaceStability.Unstable
public final class ExitUtil {
- private final static Log LOG = LogFactory.getLog(ExitUtil.class.getName());
+ private static final Logger
+ LOG = LoggerFactory.getLogger(ExitUtil.class.getName());
private static volatile boolean systemExitDisabled = false;
private static volatile boolean systemHaltDisabled = false;
private static volatile ExitException firstExitException;
private static volatile HaltException firstHaltException;
- public static class ExitException extends RuntimeException {
+ private ExitUtil() {
+ }
+
+ /**
+ * An exception raised when a call to {@link #terminate(int)} was
+ * called and system exits were blocked.
+ */
+ public static class ExitException extends RuntimeException
+ implements ExitCodeProvider {
private static final long serialVersionUID = 1L;
+ /**
+ * The status code.
+ */
public final int status;
public ExitException(int status, String msg) {
super(msg);
this.status = status;
}
+
+ public ExitException(int status,
+ String message,
+ Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ }
+
+ public ExitException(int status, Throwable cause) {
+ super(cause);
+ this.status = status;
+ }
+
+ @Override
+ public int getExitCode() {
+ return status;
+ }
+
+ /**
+ * String value does not include exception type, just exit code and message.
+ * @return the exit code and any message
+ */
+ @Override
+ public String toString() {
+ String message = getMessage();
+ if (message == null) {
+ message = super.toString();
+ }
+ return Integer.toString(status) + ": " + message;
+ }
}
- public static class HaltException extends RuntimeException {
+ /**
+ * An exception raised when a call to {@link #terminate(int)} was
+ * called and system halts were blocked.
+ */
+ public static class HaltException extends RuntimeException
+ implements ExitCodeProvider {
private static final long serialVersionUID = 1L;
public final int status;
+ public HaltException(int status, Throwable cause) {
+ super(cause);
+ this.status = status;
+ }
+
public HaltException(int status, String msg) {
super(msg);
this.status = status;
}
+
+ public HaltException(int status,
+ String message,
+ Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ }
+
+ @Override
+ public int getExitCode() {
+ return status;
+ }
+
+ /**
+ * String value does not include exception type, just exit code and message.
+ * @return the exit code and any message
+ */
+ @Override
+ public String toString() {
+ String message = getMessage();
+ if (message == null) {
+ message = super.toString();
+ }
+ return Integer.toString(status) + ": " + message;
+ }
+
}
/**
@@ -69,7 +151,7 @@ public static void disableSystemHalt() {
}
/**
- * @return true if terminate has been called
+ * @return true if terminate has been called.
*/
public static boolean terminateCalled() {
// Either we set this member or we actually called System#exit
@@ -77,21 +159,21 @@ public static boolean terminateCalled() {
}
/**
- * @return true if halt has been called
+ * @return true if halt has been called.
*/
public static boolean haltCalled() {
return firstHaltException != null;
}
/**
- * @return the first ExitException thrown, null if none thrown yet
+ * @return the first ExitException thrown, null if none thrown yet.
*/
public static ExitException getFirstExitException() {
return firstExitException;
}
/**
- * @return the first {@code HaltException} thrown, null if none thrown yet
+ * @return the first {@code HaltException} thrown, null if none thrown yet.
*/
public static HaltException getFirstHaltException() {
return firstHaltException;
@@ -110,22 +192,22 @@ public static void resetFirstHaltException() {
}
/**
- * Terminate the current process. Note that terminate is the *only* method
- * that should be used to terminate the daemon processes.
- *
- * @param status
- * exit code
- * @param msg
- * message used to create the {@code ExitException}
- * @throws ExitException
- * if System.exit is disabled for test purposes
+ * Inner termination: either exit with the exception's exit code,
+ * or, if system exits are disabled, rethrow the exception.
+ * @param ee exit exception
*/
- public static void terminate(int status, String msg) throws ExitException {
- LOG.info("Exiting with status " + status);
+ public static synchronized void terminate(ExitException ee)
+ throws ExitException {
+ int status = ee.getExitCode();
+ String msg = ee.getMessage();
+ if (status != 0) {
+ //exit indicates a problem, log it
+ LOG.debug("Exiting with status {}: {}", status, msg, ee);
+ LOG.info("Exiting with status {}: {}", status, msg);
+ }
if (systemExitDisabled) {
- ExitException ee = new ExitException(status, msg);
- LOG.fatal("Terminate called", ee);
- if (null == firstExitException) {
+ LOG.error("Terminate called", ee);
+ if (!terminateCalled()) {
firstExitException = ee;
}
throw ee;
@@ -135,20 +217,26 @@ public static void terminate(int status, String msg) throws ExitException {
/**
* Forcibly terminates the currently running Java virtual machine.
- *
- * @param status
- * exit code
- * @param msg
- * message used to create the {@code HaltException}
- * @throws HaltException
- * if Runtime.getRuntime().halt() is disabled for test purposes
+ * The exception argument is rethrown if JVM halting is disabled.
+ * @param ee the exception containing the status code, message and any stack
+ * trace.
+ * @throws HaltException if {@link Runtime#halt(int)} is disabled.
*/
- public static void halt(int status, String msg) throws HaltException {
- LOG.info("Halt with status " + status + " Message: " + msg);
+ public static synchronized void halt(HaltException ee) throws HaltException {
+ int status = ee.getExitCode();
+ String msg = ee.getMessage();
+ try {
+ if (status != 0) {
+ //exit indicates a problem, log it
+ LOG.debug("Halt with status {}: {}", status, msg, ee);
+ LOG.info("Halt with status {}: {}", status, msg, msg);
+ }
+ } catch (Exception ignored) {
+ // ignore exceptions here, as it may be due to an out of memory situation
+ }
if (systemHaltDisabled) {
- HaltException ee = new HaltException(status, msg);
- LOG.fatal("Halt called", ee);
- if (null == firstHaltException) {
+ LOG.error("Halt called", ee);
+ if (!haltCalled()) {
firstHaltException = ee;
}
throw ee;
@@ -157,47 +245,94 @@ public static void halt(int status, String msg) throws HaltException {
}
/**
- * Like {@link terminate(int, String)} but uses the given throwable to
- * initialize the ExitException.
- *
- * @param status
- * @param t
- * throwable used to create the ExitException
- * @throws ExitException
- * if System.exit is disabled for test purposes
+ * Like {@link #terminate(int, String)} but uses the given throwable to
+ * build the message to display or throw as an
+ * {@link ExitException}.
+ * S run(S service) {
+ assertNotNull(service);
+ service.init(new Configuration());
+ service.start();
+ return service;
+ }
+
+ /**
+ * Save a configuration to a config file in the target dir.
+ * @param conf config
+ * @return absolute path
+ * @throws IOException problems
+ */
+ protected String configFile(Configuration conf) throws IOException {
+ File directory = new File(CONF_FILE_DIR);
+ directory.mkdirs();
+ File file = File.createTempFile("conf", ".xml", directory);
+ try(OutputStream fos = new FileOutputStream(file)) {
+ conf.writeXml(fos);
+ }
+ return file.getAbsolutePath();
+ }
+
+ /**
+ * Create a new config from key-val pairs.
+ * @param kvp a list of key, value, ...
+ * @return a new configuration
+ */
+ protected Configuration newConf(String... kvp) {
+ int len = kvp.length;
+ assertEquals("unbalanced keypair len of " + len, 0, len % 2);
+ Configuration conf = new Configuration(false);
+ for (int i = 0; i < len; i += 2) {
+ conf.set(kvp[i], kvp[i + 1]);
+ }
+ return conf;
+ }
+
+ /** varargs to list conversion. */
+ protected List service type
+ * @return the service launcher
+ * @throws ExitUtil.ExitException if the launch's exit code != 0
+ */
+ protected ServiceLauncher launchService(
+ Class serviceClass,
+ Configuration conf,
+ List serviceLauncher =
+ new ServiceLauncher<>(serviceClass.getName());
+ ExitUtil.ExitException exitException =
+ serviceLauncher.launchService(conf, args, false, execute);
+ if (exitException.getExitCode() == 0) {
+ // success
+ return serviceLauncher;
+ } else {
+ // launch failure
+ throw exitException;
+ }
+ }
+
+ /**
+ * Launch a service with the given list of arguments. Returns
+ * the service launcher, from which the created service can be extracted.
+ * via {@link ServiceLauncher#getService()}.
+ *
+ * This call DOES NOT call {@link LaunchableService#execute()} or wait for
+ * a simple service to finish. It returns the service that has been created,
+ * initialized and started.
+ * @param serviceClass service class to create
+ * @param conf configuration
+ * @param args varargs launch arguments
+ * @param service type
+ * @return the service launcher
+ * @throws ExitUtil.ExitException if the launch's exit code != 0
+ */
+ protected ServiceLauncher launchService(
+ Class serviceClass,
+ Configuration conf,
+ String... args) throws ExitUtil.ExitException {
+ return launchService(serviceClass, conf, Arrays.asList(args), false);
+ }
+
+ /**
+ * Launch expecting an exception.
+ * @param serviceClass service class to create
+ * @param conf configuration
+ * @param expectedText expected text; may be "" or null
+ * @param errorCode error code
+ * @param args varargs launch arguments
+ * @return the exception returned if there was a match
+ * @throws AssertionError on a mismatch of expectation and actual
+ */
+ protected ExitUtil.ExitException launchExpectingException(Class serviceClass,
+ Configuration conf,
+ String expectedText,
+ int errorCode,
+ String... args) {
+ try {
+ ServiceLauncher type of service to launch
+ */
+public class ExitTrackingServiceLauncher extends
+ ServiceLauncher {
+
+ private ExitUtil.ExitException exitException;
+
+ public ExitTrackingServiceLauncher(String serviceClassName) {
+ super(serviceClassName);
+ }
+
+ @Override
+ protected void exit(ExitUtil.ExitException ee) {
+ exitException = ee;
+ super.exit(ee);
+ }
+
+ @Override
+ protected void exit(int exitCode, String message) {
+ exit(new ServiceLaunchException(exitCode, message));
+ }
+
+ public void bindCommandOptions() {
+ super.bindCommandOptions();
+ }
+
+ public ExitUtil.ExitException getExitException() {
+ return exitException;
+ }
+}
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceConf.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceConf.java
new file mode 100644
index 0000000000..6eb6372dcd
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceConf.java
@@ -0,0 +1,146 @@
+/*
+ * 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.conf.Configuration;
+import org.apache.hadoop.service.Service;
+import org.apache.hadoop.service.launcher.testservices.LaunchableRunningService;
+import org.apache.hadoop.service.launcher.testservices.RunningService;
+import static org.apache.hadoop.service.launcher.LauncherArguments.*;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.util.List;
+
+/**
+ * Test how configuration files are loaded off the command line.
+ */
+public class TestServiceConf
+ extends AbstractServiceLauncherTestBase {
+
+ @Test
+ public void testRunService() throws Throwable {
+ assertRuns(LaunchableRunningService.NAME);
+ }
+
+ @Test
+ public void testConfPropagationOverInitBindings() throws Throwable {
+ Configuration conf = newConf(RunningService.FAIL_IN_RUN, "true");
+ assertLaunchOutcome(EXIT_FAIL,
+ "failed",
+ LaunchableRunningService.NAME,
+ ARG_CONF_PREFIXED,
+ configFile(conf));
+ }
+
+ @Test
+ public void testUnbalancedConfArg() throws Throwable {
+ assertLaunchOutcome(EXIT_COMMAND_ARGUMENT_ERROR,
+ E_PARSE_FAILED,
+ LaunchableRunningService.NAME,
+ ARG_CONF_PREFIXED);
+ }
+
+ @Test
+ public void testConfArgMissingFile() throws Throwable {
+ assertLaunchOutcome(EXIT_COMMAND_ARGUMENT_ERROR,
+ E_PARSE_FAILED,
+ LaunchableRunningService.NAME,
+ ARG_CONF_PREFIXED,
+ "no-file.xml");
+ }
+
+ @Test
+ public void testConfPropagation() throws Throwable {
+ Configuration conf = newConf(RunningService.FAIL_IN_RUN, "true");
+ assertLaunchOutcome(EXIT_EXCEPTION_THROWN,
+ RunningService.FAILURE_MESSAGE,
+ RunningService.NAME,
+ ARG_CONF_PREFIXED,
+ configFile(conf));
+ }
+
+ /**
+ * Low level conf value extraction test...just to make sure
+ * that all works at the lower level.
+ * @throws Throwable
+ */
+ @Test
+ public void testConfExtraction() throws Throwable {
+ ExitTrackingServiceLauncher
+ *
+ */
+public class LaunchableRunningService extends RunningService implements
+ LaunchableService {
+ public static final String NAME =
+ "org.apache.hadoop.service.launcher.testservices.LaunchableRunningService";
+ public static final String ARG_FAILING = "--failing";
+ public static final String EXIT_CODE_PROP = "exit.code";
+ private static final Logger LOG =
+ LoggerFactory.getLogger(LaunchableRunningService.class);
+ private int exitCode = 0;
+
+ public LaunchableRunningService() {
+ this("LaunchableRunningService");
+ }
+
+ public LaunchableRunningService(String name) {
+ super(name);
+ }
+
+ @Override
+ public Configuration bindArgs(Configuration config, List