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..80a3811 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,28 +20,51 @@ 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. */ @Public @Evolving -public class ServiceStateException extends RuntimeException { +public class ServiceStateException extends RuntimeException implements + ExitCodeProvider { private static final long serialVersionUID = 1110000352259232646L; + /** + * Exit code. Non final as the constructor logic is too complex for + * the compiler to like. + */ + private int exitCode; public ServiceStateException(String message) { - super(message); + this(message, null); } public ServiceStateException(String message, Throwable cause) { + this(LauncherExitCodes.EXIT_SERVICE_LIFECYCLE_EXCEPTION, message, cause); + } + + private ServiceStateException(int exitCode, + String message, + Throwable cause) { super(message, cause); + + this.exitCode = (cause instanceof ExitCodeProvider)? + (((ExitCodeProvider) cause).getExitCode()) + : 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 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..7009307 --- /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 org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.AbstractService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Subclass of {@link AbstractService} that provides basic implementations + * of the new 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 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} + *

+ * 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 0000000..b7595f0 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/HadoopUncaughtExceptionHandler.java @@ -0,0 +1,127 @@ +/** + * 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.Public; +import org.apache.hadoop.classification.InterfaceStability.Evolving; +import org.apache.hadoop.util.ExitUtil; +import org.apache.hadoop.util.ShutdownHookManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.Thread.UncaughtExceptionHandler; + +/** + * 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 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 0000000..b6d8a1b --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/InterruptEscalator.java @@ -0,0 +1,203 @@ +/* + * 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 com.google.common.base.Preconditions; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +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. + *

    + *
  1. The service is given a time in milliseconds to stop: + * if it exceeds this it the process exits anyway.
  2. + *
  3. the exit operation used is {@link ServiceLauncher#exit(int, String)} + * with the exit code {@link LauncherExitCodes#EXIT_INTERRUPTED}
  4. + *
  5. 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.
  6. + *
+ * + */ + +@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 WeakReference ownerRef; + + private final int shutdownTimeMillis; + + /** + * Previous interrupt handlers. These are not queried. + */ + private final List 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 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(); + } + } +} 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..22dc6cd --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/IrqHandler.java @@ -0,0 +1,170 @@ +/* + * 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 com.google.common.base.Preconditions; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sun.misc.Signal; +import sun.misc.SignalHandler; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Handler of interrupts -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. + */ +@InterfaceAudience.Private +@InterfaceStability.Unstable +@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 kill signal: {@value} + */ + public static final String SIGTERM = "TERM"; + + /** + * Signal name + */ + private final String name; + + /** + * Handler to relay to + */ + private final Interrupted handler; + + 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 + */ + protected 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 { + public final String name; + public final int number; + + public InterruptData(String name, int number) { + this.name = name; + this.number = 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 0000000..9507aa8 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LaunchableService.java @@ -0,0 +1,92 @@ +/* + * 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; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.Service; + +import java.util.List; + +/** + * An interface which services can implement to have their + * execution managed by the ServiceLauncher. + * 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 args) throws Exception; + + /** + * Run a service -called after {@link Service#start()}. + *

+ * The return value becomes the exit code of the launched process. + *

+ * If an exception is raised, the policy is: + *

    + *
  1. Any subset of {@link org.apache.hadoop.util.ExitUtil.ExitException}: + * the exception is passed up unmodified + *
  2. + *
  3. 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.
  4. + *
  5. 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)
  6. + *
+ * @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 0000000..8c2590c --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherArguments.java @@ -0,0 +1,45 @@ +/* + * 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 GenericOptionsParser -simply extracted to constants + */ +public interface LauncherArguments { + /** + * Name of the configuration argument on the CLI: {@value} + */ + String ARG_CONF = "conf"; + + /** + * Name of a configuration class which is loaded before any + * attempt is made to load the class. + *

+ * {@value} + */ + String ARG_CONFCLASS = "confclass"; + + /** + * Error string on a parse failure + *

+ * {@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 0000000..48b2ef1 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/LauncherExitCodes.java @@ -0,0 +1,107 @@ +/* + * 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. + *

+ * Exit codes from 32 up are defined for the {@link ServiceLauncher}. + * These are away up from the the base numbers, to distinguish them. + * Applications can have their own set of failures -it is recommended to + * start them at 64 to differentiate them. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving + +public interface LauncherExitCodes { + /** + * 0: success + */ + int EXIT_SUCCESS = 0; + + /** + * -1: generic "false" response. The operation worked but + * the result was not true + */ + int EXIT_FAIL = -1; + + /** + * YARN Exit code on a client initiated AM termination: {@value} + */ + int EXIT_CLIENT_INITIATED_SHUTDOWN = 1; + + /** + * YARN Exit code when tasks could not be launched: {@value} + */ + int EXIT_TASK_LAUNCH_FAILURE = 2; + + /** + * Exit code when an exception was thrown from the service: {@value} + */ + int EXIT_EXCEPTION_THROWN = 32; + + /** + * Exit code when a usage message was printed: {@value} + */ + int EXIT_USAGE = 33; + + /** + * Exit code when something happened but we can't be specific: {@value} + */ + int EXIT_OTHER_FAILURE = 34; + + /** + * Exit code when a control-C, kill -3, signal was picked up: {@value} + */ + + int EXIT_INTERRUPTED = 35; + + /** + * Exit code when the command line doesn't parse: {@value}, or + * when it is otherwise invalid. + */ + int EXIT_COMMAND_ARGUMENT_ERROR = 36; + + /** + * Exit code when the configurations in valid/incomplete: {@value} + */ + int EXIT_BAD_CONFIGURATION = 37; + + /** + * Exit code on network connectivity problems: {@value} + */ + int EXIT_CONNECTIVITY_PROBLEM = 38; + + /** + * the service instance could not be created: {@value} + */ + int EXIT_SERVICE_CREATION_FAILURE = 39; + + /** + * the service instance could not be created: {@value} + */ + int EXIT_SERVICE_LIFECYCLE_EXCEPTION = 40; + +} 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..8bca0d3 --- /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 org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.util.ExitCodeProvider; +import org.apache.hadoop.util.ExitUtil; + +import java.util.Locale; + +/** + * 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 is still be available 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 0000000..48c7bbf --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceLauncher.java @@ -0,0 +1,1013 @@ +/* + * 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 com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A class to launch any service by name. + *

+ * It's designed to be subclassed for custom entry points. + *

+ * + * Workflow + *

    + *
  1. An instance of the class is created. It must be of the type + * {@link Service}
  2. + *
  3. If it implements {@link LaunchableService}, + * it is given the binding args off the CLI
  4. + *
  5. Its Service.init() and Service.start() + * methods are called.
  6. + *
  7. If it implements {@link LaunchableService}, runService() + * is called and its return code used as the exit code.
  8. + *
  9. Otherwise: it waits for the service to stop, assuming that the + * start() method spawns one or more thread to perform work
  10. + *
  11. If any exception is raised and provides an exit code -that is it implements + * {@link ExitCodeProvider}- that becomes the exit code of the + * command.
  12. + *
+ * Error and warning messages are logged to stderr. If the classpath + * is wrong and logger configurations not on it, then no error messages by + * the started app will be seen and the caller is left trying to debug + * using exit codes. + * + * @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"; + + /** + * Usage message. + *

+ * {@value} + */ + public static final String USAGE_MESSAGE = + "Usage: " + NAME + " classname " + + "[" + ARG_CONF + " ] " + + " "; + + /** + * 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; + + /** + * 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 resources to load to configuration before instantiating class + */ + private List confResources = new ArrayList<>(); + + /** + * List of the standard configurations to create (and so load in properties) + */ + protected static final String[] defaultConfigs = { + "org.apache.hadoop.conf.Configuration", + "org.apache.hadoop.hdfs.HdfsConfiguration", + "org.apache.hadoop.yarn.conf.YarnConfiguration" + }; + + /** + * 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 + confResources.addAll(Arrays.asList(defaultConfigs)); + } + + /** + * 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 service the new service + */ + protected void setService(S service) { + this.service = service; + } + + /** + * 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 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 (serviceClassName != null && !serviceClassName.isEmpty()) { + sb.append("serviceClassName='").append(serviceClassName).append('\''); + } + if (service != null) { + sb.append(", service=").append(service); + } + return sb.toString(); + } + + /** + * Launch the service and exit. + *

+ *

    + *
  1. Parse the command line.
  2. + *
  3. Build the service configuration from it.
  4. + *
  5. Start the service.
  6. . + *
  7. If it is a {@link LaunchableService}: execute it
  8. + *
  9. Otherwise: wait for it to finish.
  10. + *
  11. Exit passing the status code to the {@link #exit(int, String)} + * method.
  12. + *
+ * @param args arguments to the service. arg[0] is + * assumed to be the service classname. + */ + public void launchServiceAndExit(List args) { + + if (LOG.isDebugEnabled()) { + LOG.debug(startupShutdownMessage(serviceName, args)); + StringBuilder builder = new StringBuilder(); + for (String arg : args) { + builder.append('"').append(arg).append("\" "); + } + LOG.debug(builder.toString()); + } + registerFailureHandling(); + // set up the configs, using reflection to push in the -site.xml files + createDefaultConfigs(); + Configuration conf = createConfiguration(); + List processedArgs = extractCommandOptions(conf, args); + ExitUtil.ExitException ee = launchService(conf, processedArgs, true, true); + System.out.flush(); + System.err.flush(); + exit(ee); + } + + /** + * Override point: create an options instance to combine with the + * standard options set. + * @return the new options. + */ + protected Options createOptions() { + Options options = new Options(); + options.addOption(null, ARG_CONF, true, ARG_CONF); + return options; + } + + /** + * Add the core Hadoop tool options, except + * those added for MR jobs: libjars, + * files and archives + */ + @SuppressWarnings("static-access") + protected static Options buildHadoopToolOptions(Options opts) { + Option fs = OptionBuilder.withArgName("filesystem URI") + .hasArg() + .withDescription("specify a filesystem") + .create("fs"); + Option oconf = OptionBuilder.withArgName("configuration file") + .hasArg() + .withDescription("specify an application configuration file") + .create("conf"); + Option property = OptionBuilder.withArgName("property=value") + .hasArg() + .withDescription("use value for given property") + .create('D'); + + + // file with security tokens + Option tokensFile = OptionBuilder.withArgName("tokensFile") + .hasArg() + .withDescription("name of the file with the tokens") + .create("tokenCacheFile"); + + opts.addOption(fs); + opts.addOption(oconf); + opts.addOption(property); + opts.addOption(tokensFile); + + return opts; + } + + /** + * 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 initalizer. + */ + protected Configuration createConfiguration() { + return new Configuration(); + } + + /** + * Override point: Get a list of configurations to create. + * @return the array of configs to attempt to create. If any are off the + * classpath, that is logged + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + protected List getConfigurationsToCreate() { + return confResources; + } + + /** + * This creates all the default configurations, 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 Configuration + * @throws ExitUtil.ExitException if a loaded class is of the wrong type + */ + @VisibleForTesting + public int createDefaultConfigs() { + List 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 +"- 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 {} -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 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); + } + serviceExitCode = exitException.getExitCode(); + serviceException = 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 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 launchedService, pass in the conf and arguments before init) + LOG.debug("Service {} implements LaunchedService", name); + launchableService = (LaunchableService) service; + if (launchableService.isInState(Service.STATE.INITED)) { + LOG.warn("LaunchedService {}" + + " 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 serviceClassName. + *

+ * Sets the configuration field + * to the the value of conf, + * and the 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 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 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. + * + *

    + *
  1. If is already the right type, pass it through.
  2. + *
  3. If it implements {@link ExitCodeProvider#getExitCode()}, + * the exit code is extracted and used in the new exception.
  4. + *
  5. Otherwise, the exit code + * {@link LauncherExitCodes#EXIT_EXCEPTION_THROWN} is used.
  6. + *
+ * + * @param thrown the exception thrown + * @return an ExitException with a status code + */ + protected static ExitUtil.ExitException convertToExitException(Throwable thrown) { + ExitUtil.ExitException + exitException;// other exceptions are converted to ExitExceptions + // 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); + } + + /** + * Register this class as the handler for the control-C interrupt. + *

+ * 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. + *

+ * 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 LOG.error() + *

+ * If that log level is disabled disabled the message + * is logged to system error along + * with 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 + * 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. + *

+ * This is the standard way a launched service exits. + * An error code of 0 means success -nothing is printed. + *

+ * By default, calls + * {@link ExitUtil#terminate(ExitUtil.ExitException)}. + *

+ * This can be subclassed for testing + * @param ee exit exception + */ + 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. 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 extractCommandOptions(Configuration conf, + List args) { + int size = args.size(); + if (size <= 1) { + return new ArrayList<>(0); + } + List coreArgs = args.subList(1, size); + + return parseCommandArgs(conf, createOptions(), coreArgs); + } + + public List extractConfigurationArgs1(Configuration conf, + List args) { + + int size = args.size(); + if (size <= 1 ) { + return new ArrayList<>(0); + } + List argsList = new ArrayList<>(size); + //skip that first entry + int index = 1; + while (index< size) { + String arg = args.get(index); + LOG.info("arg[{}]={}", index, arg); + if (arg.equals(ARG_CONF)) { + //the argument is a --conf file tuple: extract the path and load + //it in as a configuration resource. + + //increment the loop iterator + index++; + if (index == size) { + //overshot the end of the file + exitWithMessage(EXIT_COMMAND_ARGUMENT_ERROR, + ARG_CONF + ": missing configuration file"); + // never called, but retained for completeness + break; + } + + String filename = args.get(index); + index++; + LOG.info("arg[{}] = Conf file {}",index, filename); + URL fileURL = extractConfFile(filename); + conf.addResource(fileURL); + } else { + index++; + argsList.add(arg); + } + } + return argsList; + } + + /** + * Extract a {@link Configuration} file from the filename argument. + *

+ * This includes tests that the file exists and is a valid + * XML Configuration file. + *

+ * If the load fails for any reason, the JVM is exited with an error code + * {@link LauncherExitCodes#EXIT_COMMAND_ARGUMENT_ERROR} and a message. + * @param filename filename argument + * @return the URL to the file. + */ + private URL extractConfFile(String filename) { + File file = new File(filename); + URL fileURL = null; + if (!file.exists()) { + // no configuration file + exitWithMessage(EXIT_COMMAND_ARGUMENT_ERROR, + ARG_CONF + ": configuration file not found: " + file); + } else { + try { + Configuration c = new Configuration(false); + fileURL = file.toURI().toURL(); + c.addResource(fileURL); + c.get("key"); + } catch (Exception e) { + // this means that the file could not be parsed + exitWithMessage(EXIT_COMMAND_ARGUMENT_ERROR, + ARG_CONF + ": configuration file not loadable: " + file); + } + } + return fileURL; + } + + /** + * Parse the command arguments, extracting the service class as the last + * element of the list (after extracting all the rest) + * @param conf configuration to use + * @param options custom options; pass in an empty Options if unused + * @param args command line argument list + * @return the remaining arguments + * @throws ServiceLaunchException if processing of arguments failed + */ + @VisibleForTesting + public List parseCommandArgs(Configuration conf, + Options options, + List args) { + StringBuilder argString = new StringBuilder(args.size() * 32); + for (String arg : args) { + LOG.debug(arg); + argString.append("\"").append(arg).append("\" "); + } + try { + GenericOptionsParser parser; + 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 + parser = new GenericOptionsParser(conf, options, argArray); + if (!parser.isParseSuccessful()) { + throw new ServiceLaunchException(EXIT_COMMAND_ARGUMENT_ERROR, + E_PARSE_FAILED + " %s", argString); + } + CommandLine line = parser.getCommandLine(); + + // Scan the list of configuration files + // and bail out if they don't exist + if (line.hasOption(ARG_CONF)) { + verifyConfigurationFilesExist(line.getOptionValues(ARG_CONF)); + } + if (line.hasOption(ARG_CONFCLASS)) { + // new resources to instantiate as configurations + String[] confClassnames = line.getOptionValues(ARG_CONFCLASS); + confResources.addAll(Arrays.asList(confClassnames)); + } + // then extract the remainder + return Arrays.asList(parser.getRemainingArgs()); + } 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); + } + } + + 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_COMMAND_ARGUMENT_ERROR, + ARG_CONF + ": 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 + */ + public static String startupShutdownMessage(String classname, + List 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 + */ + public static void exitWithMessage(int status, String message) { + ExitUtil.terminate(new ServiceLaunchException(status, message)); + } + + /** + * Exit with the usage exit code and message + * @throws ExitUtil.ExitException if exceptions are disabled + */ + public 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. + *

    + *
  1. -conf <file> : configuration file
  2. + *
+ *

+ * Argument 0 MUST be the service classname + * @param argsList the list of arguments + */ + /* ====================================================================== */ + + public static void serviceMain(List argsList) { + if (argsList.isEmpty()) { + // no arguments: usage message + exitWithUsageMessage(); + } else { + ServiceLauncher serviceLauncher = + new ServiceLauncher<>(argsList.get(0)); + serviceLauncher.launchServiceAndExit(argsList); + } + } + +} 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..12863de --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/ServiceShutdownHook.java @@ -0,0 +1,117 @@ +/* + * 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; +import org.apache.hadoop.service.Service; +import org.apache.hadoop.util.ShutdownHookManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.ref.WeakReference; + +/** + * 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, so + * does not cause services to be retained after they have + * been stopped and deferenced elsewhere. + */ +@InterfaceAudience.Private +@InterfaceStability.Unstable + +/** + * A service shutdown hook. + *

+ * It stores a weak reference to a service, and, when shut down, + * calls {@link Service#stop()} if the reference is valid. + */ +public class ServiceShutdownHook implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger( + ServiceShutdownHook.class); + + /** + * A weak reference to the service. + */ + private final WeakReference 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. + *

+ * 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 0000000..38bab80 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/service/launcher/package-info.java @@ -0,0 +1,418 @@ +/* + * 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. + +

Key Features

+ +

+ 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 main(String[]) methods implemented by other classes + that which to provide their own endpoints. +

+ +

+ Extended YARN Service Interface:

+ The {@link org.apache.hadoop.service.launcher.LaunchableService} interface + extendss {@link org.apache.hadoop.service.Service} with methods to pass + down the CLI arguments, to execute an operapation 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 our or a second interrupt is received. +

+ +

Tests:

test cases include interrupt handling and + lifecycle failures.

+ +

Launching a YARN Service

+ + The Service Launcher can launch any YARN service. It will instantiate + the service classname provided, by zero-args constructor. Or, if none + is available, falling back to a constructor that takes a String + as its parameter, on the assumption that the parameter is the service name. +

+ + 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 + STATE.STOPPED, escalated into a non-zero return code. +

+ + To view the worklow in sequence, it is: +

    +
  1. (prepare configuration files -covered later)
  2. +
  3. instantiate service via its empty or string constructor
  4. +
  5. call {@link org.apache.hadoop.service.Service#init(Configuration)}
  6. +
  7. call {@link org.apache.hadoop.service.Service#start()}
  8. +
  9. call {@link org.apache.hadoop.service.Service#waitForServiceToStop(long)}
  10. +
  11. If an exception was raised: propagate it
  12. +
  13. If an exception was recorded in {@link org.apache.hadoop.service.Service#getFailureCause()} + while the service was running: propagate it.
  14. +
+ + For a service to be fully compatible with this launch model, it must +
    +
  • Start worker threads, processes and executors in its + {@link org.apache.hadoop.service.Service#start()} method
  • +
  • Terminate itself via a call to {@link org.apache.hadoop.service.Service#stop()} + in one of these asynchronous methods.
  • +
+ + 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 kill + and control-C signals -calling stop() on the service when signalled. + This means that a daemon service may get some warning and time to shut + down. + +

+ To summarize: provided a service launches its long-lived threads in its Service + start() method, the service launcher can create it, configure it + and start it -triggering shutdown when signalled. + + What these services can not do is get at the command line parameters or easily + propagate exit codes (there is way covered later). These features require + some extensions to the base Service interface: the Launchable + Service. + +

Launching a Launchable YARN Service

+ + A Launchable YARN Service is a YARN service which implements the interface + {@link org.apache.hadoop.service.launcher.LaunchableService}. +

+ It adds two methods to the service interface -and hence two new features: + +

    +
  1. Access to the command line passed to the service launcher
  2. +
  3. A blocking int execute() method which can return the exit + code for the application.
  4. +
+ + 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, so integrating with YARN's + workflow and YarnClient client-side code. + +

+ 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 execute() method. + It doesn't even need to 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 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, + so allowing services to tune their configuration data before passing it to + any superclass in that 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 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 init() method. +

+ After the bindArgs() processing, the service's init() + and 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 execute() operation completes, the + service is stopped and exit codes generated. Any exception raised + during the execute() method takes priority over any exit codes returned + by the method --so services may signal failures simply by returning + exceptions with exit codes. +

+ +

+ To view the worklow in sequence, it is: +

    +
  1. (prepare configuration files --covered later)
  2. +
  3. instantiate service via its empty or string constructor
  4. +
  5. call {@link org.apache.hadoop.service.launcher.LaunchableService#bindArgs(Configuration, List)}
  6. +
  7. call {@link org.apache.hadoop.service.Service#init(Configuration)} with the existing config, + or any new one returned by {@link org.apache.hadoop.service.launcher.LaunchableService#bindArgs(Configuration, List)}
  8. +
  9. call {@link org.apache.hadoop.service.Service#start()}
  10. +
  11. call {@link org.apache.hadoop.service.launcher.LaunchableService#execute()}
  12. +
  13. call {@link org.apache.hadoop.service.Service#stop()}
  14. +
  15. The return code from the {@link org.apache.hadoop.service.launcher.LaunchableService#execute()} + becomes the exit code of the process, unless overridden by an exception.
  16. +
  17. If an exception was raised in this workflow: propagate it
  18. +
  19. If an exception was recorded in {@link org.apache.hadoop.service.Service#getFailureCause()} + while the service was running: propagate it.
  20. +
+ + +

Exit codes and Exceptions

+ +

+ 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 LaunchableServices 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 {@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 such like. There are also some common exit codes + for Hadoop/YARN service failures, such as {@link org.apache.hadoop.service.launcher.LauncherExitCodes#EXIT_CONNECTIVITY_PROBLEM}. + 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: +

    +
  1. If no exception was triggered by a basic service, a + {@link org.apache.hadoop.service.launcher.ServiceLaunchException} with an + exit code of 0 s created.
  2. +
  3. For a LaunchableService, the exit code is the result of execute(). + Again, a {@link org.apache.hadoop.service.launcher.ServiceLaunchException} + with a return code of 0 is created. +
  4. +
  5. Otherwise, if the exception is an instance of ExitException, + it is returned as the service terminating exception.
  6. +
  7. If the exception implements {@link org.apache.hadoop.util.ExitCodeProvider}, + its exit code and getMessage() value become the exit exception.
  8. +
  9. Otherwise, it is wrapped as a {@link org.apache.hadoop.service.launcher.ServiceLaunchException} + with the exit code {@link org.apache.hadoop.service.launcher.LauncherExitCodes#EXIT_EXCEPTION_THROWN} + to indicate that an exception was thrown.
  10. +
  11. This is finally passed to {@link org.apache.hadoop.util.ExitUtil#terminate(ExitUtil.ExitException)}, + by way of {@link org.apache.hadoop.service.launcher.ServiceLauncher#exit(ExitUtil.ExitException)}; + a method designed to allow subclasses to override for testing.
  12. +
  13. The {@link org.apache.hadoop.util.ExitUtil} class then terminates the JVM + with the specified exit code, printing the toString() value + of the exception if the return code is non-zero.
  14. + + This process may seem convoluted, but it is designed to allow any exception in the + Hadoop exception hierarchy to generate exit codes, and to minimise the amount of + exception wrapping which takes place. + +

    Interrupt Handling

    + + The Service Launcher has a helper class, the {@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, then triggers a JVM shutdown + by way of {@link org.apache.hadoop.util.ExitUtil#terminate(int, String)} +

    + 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. +

    + +

    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 HdfsConfiguration or YarnConfiguration. +

    + 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 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 main() method. + +

    2: Creation in bindArgs()

    + +

    + In the {@link org.apache.hadoop.service.launcher.LaunchableService#bindArgs(Configuration, List)}, + a new configuration is created: + +

    + public Configuration bindArgs(Configuration config, List args) throws
    + Exception {
    +   Configuration newConf = new YarnConfiguration(config);
    +   return newConf;
    + }
    + 
    + + 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. + +

    3: Creation in serviceInit()

    + +
    + protected void serviceInit(Configuration conf) throws Exception {
    +   super.serviceInit(new YarnConfiguration(conf));
    + }
    + 
    +

    + 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 serviceInit() is the recommended policy. + +

    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: --conf <file>. This must refer to a file + in the local filesystem which exists. 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 --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. + + +

    Utility Classes

    + +
      + +
    • + {@link org.apache.hadoop.service.launcher.IrqHandler}: registers interrupt + handlers using sun.misc APIs. +
    • + +
    • + {@link org.apache.hadoop.service.launcher.ServiceLaunchException}: a + subclass of {@link org.apache.hadoop.util.ExitUtil.ExitException} which + takes a String-formatted format string and a list of arguments to create + the exception text. +
    • + +
    + */ + + +package org.apache.hadoop.service.launcher; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.util.ExitUtil; + +import java.util.List; 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 0000000..2fd6150 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ExitCodeProvider.java @@ -0,0 +1,34 @@ +/* + * 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 5208927..096a37f 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,120 @@ */ 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 { + /** + * 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; + } + } /** @@ -110,22 +189,21 @@ 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 +213,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 - */ - public static void halt(int status, String msg) throws HaltException { - LOG.info("Halt with status " + status + " Message: " + msg); + * The exception argument is rethrown if JVM halitng 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 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 +241,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. + * Like {@link #terminate(int, String)} but uses the given throwable to + * Used to build the message to display or throw as an + * {@link ExitException} * - * @param status - * @param t - * throwable used to create the ExitException - * @throws ExitException - * if System.exit is disabled for test purposes + * @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 925aad6..a94dbff 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 @@ -26,6 +26,7 @@ import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; @@ -115,6 +116,7 @@ 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. @@ -167,7 +169,7 @@ public GenericOptionsParser(Configuration conf, String[] args) */ public GenericOptionsParser(Configuration conf, Options options, String[] args) throws IOException { - parseGeneralOptions(options, conf, args); + parseSuccessful = parseGeneralOptions(options, conf, args); this.conf = conf; } @@ -205,6 +207,14 @@ public CommandLine getCommandLine() { } /** + * Query for the parse operation succeeded + * @return true if there was no error parsing the CLI + */ + public boolean isParseSuccessful() { + return parseSuccessful; + } + + /** * Specify properties of each generic option */ @SuppressWarnings("static-access") @@ -271,7 +281,7 @@ private void processGeneralOptions(Configuration conf, if (line.hasOption("jt")) { String optionValue = line.getOptionValue("jt"); - if (optionValue.equalsIgnoreCase("local")) { + if (optionValue.toLowerCase(Locale.ENGLISH).equals("local")) { conf.set("mapreduce.framework.name", optionValue); } @@ -477,20 +487,24 @@ private String validateFiles(String files, Configuration conf) * @param opts Options to use for parsing args. * @param conf Configuration to be modified * @param args User-specified arguments + * @return true if the parse was succesful */ - private void parseGeneralOptions(Options opts, Configuration conf, + private boolean parseGeneralOptions(Options opts, Configuration conf, String[] args) throws IOException { opts = buildGeneralOptions(opts); CommandLineParser parser = new GnuParser(); + boolean parsed = false; try { commandLine = parser.parse(opts, preProcessForWindows(args), true); processGeneralOptions(conf, commandLine); + parsed = true; } catch(ParseException e) { LOG.warn("options parsing failed: "+e.getMessage()); HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("general options are: ", opts); } + return parsed; } /** diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/StringUtils.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/StringUtils.java index 73f9c4f..a3175ce 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/StringUtils.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/StringUtils.java @@ -612,7 +612,7 @@ public static String unEscapeString(String str, char escapeChar, * @param msg content of the message * @return a message for logging */ - private static String toStartupShutdownString(String prefix, String [] msg) { + public static String toStartupShutdownString(String prefix, String [] msg) { StringBuilder b = new StringBuilder(prefix); b.append("\n/************************************************************"); for(String s : msg) @@ -648,18 +648,7 @@ static void startupShutdownMessage(Class clazz, String[] args, final String hostname = NetUtils.getHostname(); final String classname = clazz.getSimpleName(); LOG.info( - 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") } - ) + createStartupShutdownMessage(classname, hostname, args) ); if (SystemUtils.IS_OS_UNIX) { @@ -681,6 +670,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. * TraditionalBinaryPrefix symbol are case insensitive. 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 eeb1a31..74ea5aa 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"); } @@ -111,7 +118,7 @@ public void setFailOnStop(boolean failOnStop) { */ 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 0000000..a9938ce --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/AbstractServiceLauncherTestBase.java @@ -0,0 +1,346 @@ +/* + * 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.io.IOUtils; +import org.apache.hadoop.service.Service; +import org.apache.hadoop.util.ExitCodeProvider; +import org.apache.hadoop.util.ExitUtil; +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; +import java.util.Locale; + +public class AbstractServiceLauncherTestBase extends Assert implements + LauncherExitCodes { + private static final Logger LOG = LoggerFactory.getLogger( + AbstractServiceLauncherTestBase.class); + public static final String CONF_FILE_DIR = "target/conf"; + + /** + * 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(); + } + + /** + * Formatted fail + * @param format format string + * @param args argument list. If the last argument is a throwable, it + * is used as the inner cause of the exception + * @throws AssertionError with the formatted message + */ + protected static void failf(String format, Object... args) { + String message = String.format(Locale.ENGLISH, format, args); + AssertionError error = new AssertionError(message); + int len = args.length; + if (len > 0 && args[len - 1] instanceof Throwable) { + error.initCause((Throwable) args[len - 1]); + } + throw error; + } + + /** + * Conditional formatted fail + * @param format format string + * @param args argument list. If the last argument is a throwable, it + * is used as the inner cause of the exception + * @throws AssertionError with the formatted message + */ + protected static void failif(boolean condition, + String format, + Object... args) { + if (condition) { + failf(format, args); + } + } + + /** + * rule to name the thread JUnit. + */ + @Before + public void nameThread() { + Thread.currentThread().setName("JUnit"); + } + + /** + * 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); + if (failed) { + String error = String.format( + "Expected exception with exit code %d and text \"%s\"" + + " but got the exit code %d" + + " and text \"%s\"", + expected, text, + exitCode, toString); + LOG.error(error, e); + AssertionError assertionError = new AssertionError(error); + assertionError.initCause((Throwable) e); + throw assertionError; + } + } + + /** + * 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 { + 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 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 asList(String... args) { + return Arrays.asList(args); + } + + /** + * 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()}. + * The service is has its execute() method called, but + * @param serviceClass service class to create + * @param conf configuration + * @param args list of arguments + * @param execute execute/wait for the service to stop + * @param service type + * @return the service launcher + * @throws ExitUtil.ExitException if the launch's exit code != 0 + */ + protected ServiceLauncher launchService( + Class serviceClass, + Configuration conf, + List args, + boolean execute) throws ExitUtil.ExitException { + ServiceLauncher serviceLauncher = + new ServiceLauncher<>(serviceClass.getName()); + ExitUtil.ExitException exitException = + serviceLauncher.launchService(conf, args, false, execute); + if (exitException.getExitCode() == 0) { + return serviceLauncher; + } else { + 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 launch = launchService(serviceClass, + conf, + Arrays.asList(args), + true); + + failf("Expected an exception with error code %d and text \"%s\" " + + " -but the service completed with :%s", + errorCode, expectedText, launch.getServiceException()); + return null; + } catch (ExitUtil.ExitException e) { + int actualCode = e.getExitCode(); + failif(errorCode != actualCode || + !StringUtils.contains(e.toString(), expectedText), + "Expected an exception with error code %d and text \"%s\" " + + " -but the service threw an exception with exit code %d: %s", + errorCode, expectedText, actualCode, e); + return e; + } + } + + /** + * Assert that an exception message contains a search string + * @param exception exception to get a message from + * @param search search key + */ + protected void assertMessageContains(Exception exception, String search) { + assertContains(exception.getMessage(), search); + } + + /** + * assert that a text string contains the search text + * @param text text to look through + * @param search search key + */ + protected void assertContains(String text, String search) { + failif(!StringUtils.contains(text, search), + "String \"%s\" not found in \"%s\"", text, search); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/NonExitingServiceLauncher.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/NonExitingServiceLauncher.java new file mode 100644 index 0000000..61e6c29 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/NonExitingServiceLauncher.java @@ -0,0 +1,48 @@ +/* + * 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.service.Service; +import org.apache.hadoop.util.ExitUtil; + +/** + * Service launcher for testing + * @param type of service to launch + */ +public class NonExitingServiceLauncher extends + ServiceLauncher { + + public ExitUtil.ExitException exitException; + + public NonExitingServiceLauncher(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)); + } + +} 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 0000000..217b48c --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceConf.java @@ -0,0 +1,147 @@ +/* + * 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 org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.util.List; + +public class TestServiceConf extends AbstractServiceLauncherTestBase { + + public static final String DASH_CONF = "-"+LauncherArguments.ARG_CONF; + + @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, + DASH_CONF, + configFile(conf)); + } + + @Test + public void testUnbalancedConfArg() throws Throwable { + Configuration conf = newConf(RunningService.FAIL_IN_RUN, "true"); + assertLaunchOutcome(EXIT_COMMAND_ARGUMENT_ERROR, + LauncherArguments.E_PARSE_FAILED, + LaunchableRunningService.NAME, + DASH_CONF); + } + + @Test + public void testConfArgMissingFile() throws Throwable { + Configuration conf = newConf(RunningService.FAIL_IN_RUN, "true"); + assertLaunchOutcome(EXIT_COMMAND_ARGUMENT_ERROR, + LauncherArguments.E_PARSE_FAILED, + LaunchableRunningService.NAME, + DASH_CONF, + "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, + DASH_CONF, + 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 { + ServiceLauncher launcher = + new ServiceLauncher<>(RunningService.NAME); + Configuration conf = newConf("propagated", "true"); + assertEquals("true", conf.get("propagated", "unset")); + + Configuration extracted = new Configuration(false); + + List argsList = + asList("Name", DASH_CONF, configFile(conf)); + List args = launcher.extractCommandOptions(extracted, + argsList); + if (!args.isEmpty()) { + assertEquals("args beginning with " + args.get(0), + 0, args.size()); + } + assertEquals("true", extracted.get("propagated", "unset")); + } + + /** + * Low level conf value extraction test...just to make sure + * that all works at the lower level + * @throws Throwable + */ + @Test + public void testDualConfArgs() throws Throwable { + ServiceLauncher launcher = + new ServiceLauncher<>(RunningService.NAME); + String key1 = "key1"; + Configuration conf1 = newConf(key1, "true"); + String key2 = "file2"; + Configuration conf2 = newConf(key2, "7"); + Configuration extracted = new Configuration(false); + + List argsList = + asList("Name", + DASH_CONF, configFile(conf1), + DASH_CONF, configFile(conf2)); + List args = launcher.extractCommandOptions(extracted, + argsList); + if (!args.isEmpty()) { + assertEquals("args beginning with " + args.get(0), + 0, args.size()); + } + assertTrue(extracted.getBoolean(key1, false)); + assertEquals(7, extracted.getInt(key2, -1)); + } + + + @Test + public void testConfArgWrongFiletype() throws Throwable { + File file = new File(CONF_FILE_DIR, methodName.getMethodName()); + FileWriter fileWriter = new FileWriter(file); + fileWriter.write("not-a-conf-file"); + fileWriter.close(); + assertLaunchOutcome(EXIT_COMMAND_ARGUMENT_ERROR, + "SAXParseException", + RunningService.NAME, + DASH_CONF, + file.getAbsolutePath()); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceExceptionInExecute.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceExceptionInExecute.java new file mode 100644 index 0000000..a995401 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceExceptionInExecute.java @@ -0,0 +1,62 @@ +/* + * 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 static org.apache.hadoop.service.launcher.testservices.ExceptionInExecuteLaunchableService.*; +import org.junit.Test; + +/** + * This test verifies that exceptions in the + * {@link LaunchableService#execute()} method are relayed if an instance of + * an exit exceptions, and forwarded if not. + */ +public class TestServiceExceptionInExecute extends AbstractServiceLauncherTestBase { + + @Test + public void testEx() throws Throwable { + assertLaunchOutcome(EXIT_EXCEPTION_THROWN, + OTHER_EXCEPTION_TEXT, + NAME); + } + + @Test + public void testSLE() throws Throwable { + assertLaunchOutcome(EXIT_OTHER_FAILURE, + SLE_TEXT, + NAME, + ARG_THROW_SLE); + } + + @Test + public void testIOE() throws Throwable { + assertLaunchOutcome(IOE_EXIT_CODE, + EXIT_IN_IOE_TEXT, + NAME, + ARG_THROW_IOE); + } + + @Test + public void testThrowable() throws Throwable { + assertLaunchOutcome(EXIT_EXCEPTION_THROWN, + "java.lang.OutOfMemoryError", + NAME, + ARG_THROWABLE); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceInterruptHandling.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceInterruptHandling.java new file mode 100644 index 0000000..7611274 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceInterruptHandling.java @@ -0,0 +1,117 @@ +/* + * 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.service.BreakableService; +import org.apache.hadoop.service.launcher.testservices.FailureTestService; +import org.apache.hadoop.util.ExitUtil; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test service launcher interrupt handling + */ +public class TestServiceInterruptHandling extends AbstractServiceLauncherTestBase { + private static final Logger LOG = LoggerFactory.getLogger( + TestServiceInterruptHandling.class); + + + @Test + public void testRegisterAndRaise() throws Throwable { + InterruptCatcher catcher = new InterruptCatcher(); + String name = IrqHandler.CONTROL_C; + IrqHandler irqHandler = new IrqHandler(name, catcher); + irqHandler.bind(); + assertEquals(0, irqHandler.getSignalCount()); + irqHandler.raise(); + // allow for an async event + Thread.sleep(500); + IrqHandler.InterruptData data = catcher.interruptData; + assertNotNull(data); + assertEquals(name, data.name); + assertEquals(1, irqHandler.getSignalCount()); + } + + + @Test + public void testInterruptEscalationShutdown() throws Throwable { + NonExitingServiceLauncher launcher = + new NonExitingServiceLauncher<>(BreakableService.class.getName()); + BreakableService service = new BreakableService(); + launcher.setService(service); + + InterruptEscalator escalator = + new InterruptEscalator(launcher, 500); + + // call the interrupt operation directly + try { + escalator.interrupted(new IrqHandler.InterruptData("INT", 3)); + fail("Expected an exception to be raised"); + } catch (ExitUtil.ExitException e) { + assertExceptionDetails(EXIT_INTERRUPTED, "", e); + } + //the service is now stopped + assertStopped(service); + assertTrue(escalator.isSignalAlreadyReceived()); + assertFalse(escalator.isForcedShutdownTimedOut()); + + // now interrupt it a second time and expect it to escalate to a halt + try { + escalator.interrupted(new IrqHandler.InterruptData("INT", 3)); + fail("Expected an exception to be raised"); + } catch (ExitUtil.HaltException e) { + assertExceptionDetails(EXIT_INTERRUPTED, "", e); + } + } + + + @Test + public void testBlockingShutdownTimeouts() throws Throwable { + NonExitingServiceLauncher launcher = + new NonExitingServiceLauncher<>(FailureTestService.class.getName()); + FailureTestService service = + new FailureTestService(false, false, false, 2000); + launcher.setService(service); + + InterruptEscalator escalator = + new InterruptEscalator(launcher, 500); + // call the interrupt operation directly + try { + escalator.interrupted(new IrqHandler.InterruptData("INT", 3)); + fail("Expected an exception to be raised"); + } catch (ExitUtil.ExitException e) { + assertExceptionDetails(EXIT_INTERRUPTED, "", e); + } + + assertTrue(escalator.isForcedShutdownTimedOut()); + } + + private static class InterruptCatcher implements IrqHandler.Interrupted { + + public IrqHandler.InterruptData interruptData; + + @Override + public void interrupted(IrqHandler.InterruptData data) { + LOG.info("Interrupt caught"); + this.interruptData = data; + } + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchExceptionFormatting.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchExceptionFormatting.java new file mode 100644 index 0000000..1ca8570 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchExceptionFormatting.java @@ -0,0 +1,69 @@ +/* + * 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.junit.Test; + +/** + * As the exception is doing some formatting tricks, these + * tests verify that exception arguments are being correctly + * used as initializers + */ +public class TestServiceLaunchExceptionFormatting extends + AbstractServiceLauncherTestBase { + + @Test + public void testBasicExceptionFormatting() throws Throwable { + + ServiceLaunchException ex = + new ServiceLaunchException(0, "%03x", 32); + assertMessageContains(ex, "020"); + } + + @Test + public void testNotEnoughArgsExceptionFormatting() throws Throwable { + + ServiceLaunchException ex = + new ServiceLaunchException(0, "%03x"); + assertMessageContains(ex, "%03x"); + } + + @Test + public void testInnerCause() throws Throwable { + + Exception cause = new Exception("cause"); + ServiceLaunchException ex = + new ServiceLaunchException(0, "%03x: %s", 32, cause); + assertMessageContains(ex, "020"); + assertMessageContains(ex, "cause"); + assertSame(cause, ex.getCause()); + } + + @Test + public void testInnerCauseNotInFormat() throws Throwable { + + Exception cause = new Exception("cause"); + ServiceLaunchException ex = + new ServiceLaunchException(0, "%03x:", 32, cause); + assertMessageContains(ex, "020"); + assertFalse(ex.getMessage().contains("cause")); + assertSame(cause, ex.getCause()); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchInitInConstructor.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchInitInConstructor.java new file mode 100644 index 0000000..a91455d --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchInitInConstructor.java @@ -0,0 +1,31 @@ +/* + * 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.service.launcher.testservices.InitInConstructorLaunchableService; +import org.junit.Test; + +public class TestServiceLaunchInitInConstructor extends AbstractServiceLauncherTestBase { + + @Test + public void testRunService() throws Throwable { + assertRuns(InitInConstructorLaunchableService.NAME); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchNoArgsAllowed.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchNoArgsAllowed.java new file mode 100644 index 0000000..5c32ed5 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchNoArgsAllowed.java @@ -0,0 +1,44 @@ +/* + * 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.service.launcher.testservices.NoArgsAllowedService; +import org.junit.Test; + +public class TestServiceLaunchNoArgsAllowed extends AbstractServiceLauncherTestBase { + + @Test + public void testRunService() throws Throwable { + assertRuns(NoArgsAllowedService.NAME); + } + + @Test + public void testOneArg() throws Throwable { + assertLaunchOutcome(EXIT_COMMAND_ARGUMENT_ERROR, "1", + NoArgsAllowedService.NAME, "one"); + } + + @Test + public void testConfsStripped() throws Throwable { + assertRuns(NoArgsAllowedService.NAME, + LauncherArguments.ARG_CONF, + configFile(newConf())); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchStoppingInStart.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchStoppingInStart.java new file mode 100644 index 0000000..2365f02 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchStoppingInStart.java @@ -0,0 +1,31 @@ +/* + * 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.service.launcher.testservices.StoppingInStartLaunchableService; +import org.junit.Test; + +public class TestServiceLaunchStoppingInStart extends AbstractServiceLauncherTestBase { + + @Test + public void testRunService() throws Throwable { + assertRuns(StoppingInStartLaunchableService.NAME); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchStringConstructor.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchStringConstructor.java new file mode 100644 index 0000000..ae9c31f --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchStringConstructor.java @@ -0,0 +1,31 @@ +/* + * 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.service.launcher.testservices.StringConstructorOnlyService; +import org.junit.Test; + +public class TestServiceLaunchStringConstructor extends AbstractServiceLauncherTestBase { + + @Test + public void testRunService() throws Throwable { + assertRuns(StringConstructorOnlyService.NAME); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchedRunning.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchedRunning.java new file mode 100644 index 0000000..33d2566 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLaunchedRunning.java @@ -0,0 +1,51 @@ +/* + * 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.launcher.testservices.LaunchableRunningService; +import org.junit.Test; + +public class TestServiceLaunchedRunning extends AbstractServiceLauncherTestBase { + + @Test + public void testRunService() throws Throwable { + assertRuns(LaunchableRunningService.NAME); + } + + @Test + public void testConfPropagationOverInitBindings() throws Throwable { + Configuration conf = newConf(LaunchableRunningService.FAIL_IN_RUN, "true"); + assertLaunchOutcome(EXIT_FAIL, + "failed", + LaunchableRunningService.NAME, + LauncherArguments.ARG_CONF, + configFile(conf)); + } + + @Test + public void testArgBinding() throws Throwable { + Configuration conf = newConf(LaunchableRunningService.FAIL_IN_RUN, "true"); + assertLaunchOutcome(EXIT_OTHER_FAILURE, + "", + LaunchableRunningService.NAME, + LaunchableRunningService.ARG_FAILING); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncherCreationFailures.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncherCreationFailures.java new file mode 100644 index 0000000..6e5991e --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncherCreationFailures.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 org.apache.hadoop.service.launcher.testservices.FailInConstructorService; +import org.apache.hadoop.service.launcher.testservices.FailInInitService; +import org.apache.hadoop.service.launcher.testservices.FailInStartService; +import org.apache.hadoop.service.launcher.testservices.FailingStopInStartService; +import org.junit.Test; + +public class TestServiceLauncherCreationFailures extends + AbstractServiceLauncherTestBase { + + + public static final String SELF = + "org.apache.hadoop.service.launcher.TestServiceLauncherCreationFailures"; + + @Test + public void testNoArgs() throws Throwable { + try { + ServiceLauncher.serviceMain(); + } catch (ServiceLaunchException e) { + assertExceptionDetails(EXIT_USAGE, "", e); + } + } + + @Test + public void testUnknownClass() throws Throwable { + assertServiceCreationFails("no.such.classname"); + } + + @Test + public void testNotAService() throws Throwable { + assertServiceCreationFails(SELF); + } + + @Test + public void testNoSimpleConstructor() throws Throwable { + assertServiceCreationFails( + "org.apache.hadoop.service.launcher.FailureTestService"); + } + + @Test + public void testFailInConstructor() throws Throwable { + assertServiceCreationFails(FailInConstructorService.NAME); + } + + @Test + public void testFailInInit() throws Throwable { + assertLaunchOutcome(FailInInitService.EXIT_CODE, "", + FailInInitService.NAME); + } + + @Test + public void testFailInStart() throws Throwable { + assertLaunchOutcome(FailInStartService.EXIT_CODE, "", + FailInStartService.NAME); + } + + @Test + public void testFailInStopIsIgnored() throws Throwable { + assertRuns(FailingStopInStartService.NAME); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncherInnerMethods.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncherInnerMethods.java new file mode 100644 index 0000000..0b0e83f --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncherInnerMethods.java @@ -0,0 +1,87 @@ +/* + * 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.BreakableService; +import org.apache.hadoop.service.Service; +import org.apache.hadoop.service.launcher.testservices.ExceptionInExecuteLaunchableService; +import org.apache.hadoop.service.launcher.testservices.LaunchableRunningService; +import org.apache.hadoop.service.launcher.testservices.NoArgsAllowedService; +import org.junit.Test; + +import java.util.List; + +/** + * Test the inner launcher methods. + */ +@SuppressWarnings("ThrowableResultOfMethodCallIgnored") +public class TestServiceLauncherInnerMethods extends + AbstractServiceLauncherTestBase { + + @Test + public void testLaunchService() throws Throwable { + ServiceLauncher launcher = + launchService(NoArgsAllowedService.class, new Configuration()); + } + + @Test + public void testLaunchServiceArgs() throws Throwable { + launchExpectingException(NoArgsAllowedService.class, + new Configuration(), + "arguments", EXIT_COMMAND_ARGUMENT_ERROR, + "one", "two"); + } + + @Test + public void testAccessLaunchedService() throws Throwable { + ServiceLauncher launcher = + launchService(LaunchableRunningService.class, new Configuration()); + LaunchableRunningService service = launcher.getService(); + assertInState(service, Service.STATE.STARTED); + service.failInRun = true; + service.exitCode = EXIT_CONNECTIVITY_PROBLEM; + assertEquals(EXIT_CONNECTIVITY_PROBLEM, service.execute()); + } + + @Test + public void testLaunchThrowableRaised() throws Throwable { + launchExpectingException(ExceptionInExecuteLaunchableService.class, + new Configuration(), + "java.lang.OutOfMemoryError", EXIT_EXCEPTION_THROWN, + ExceptionInExecuteLaunchableService.ARG_THROWABLE); + } + + @Test + public void testBreakableServiceLifecycle() throws Throwable { + ServiceLauncher launcher = + launchService(BreakableService.class, new Configuration()); + } + + @Test + public void testConfigLoading() throws Throwable { + ServiceLauncher launcher = + new ServiceLauncher<>("BreakableService"); + List configurationsToCreate = launcher.getConfigurationsToCreate(); + assertTrue(configurationsToCreate.size() > 1); + int created = launcher.createDefaultConfigs(); + assertEquals(1, created); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceNullBind.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceNullBind.java new file mode 100644 index 0000000..611f356 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceNullBind.java @@ -0,0 +1,31 @@ +/* + * 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.service.launcher.testservices.NullBindLaunchableService; +import org.junit.Test; + +public class TestServiceNullBind extends AbstractServiceLauncherTestBase { + + @Test + public void testRunService() throws Throwable { + assertRuns(NullBindLaunchableService.NAME); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceRunning.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceRunning.java new file mode 100644 index 0000000..40957aa --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceRunning.java @@ -0,0 +1,33 @@ +/* + * 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.service.launcher.testservices.RunningService; +import org.junit.Test; + +public class TestServiceRunning extends AbstractServiceLauncherTestBase { + + + @Test + public void testRunService() throws Throwable { + assertRuns(RunningService.NAME); + } + + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceShutdownHook.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceShutdownHook.java new file mode 100644 index 0000000..af478aa --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceShutdownHook.java @@ -0,0 +1,49 @@ +/* + * 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.service.BreakableService; +import org.junit.Test; + +public class TestServiceShutdownHook extends AbstractServiceLauncherTestBase { + + @Test + public void testShutdownHookNullReference() throws Throwable { + ServiceShutdownHook hook = new ServiceShutdownHook(null) ; + hook.run(); + } + + @Test + public void testShutdownHook() throws Throwable { + BreakableService service = new BreakableService(); + ServiceShutdownHook hook = new ServiceShutdownHook(service); + hook.run(); + assertStopped(service); + } + + @Test + public void testFailingHookCaught() throws Throwable { + BreakableService service = new BreakableService(false, false, true); + ServiceShutdownHook hook = new ServiceShutdownHook(service); + hook.run(); + assertStopped(service); + + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceStopping.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceStopping.java new file mode 100644 index 0000000..4901861 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceStopping.java @@ -0,0 +1,44 @@ +/* + * 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.launcher.testservices.FailingStopInStartService; +import org.junit.Test; + +/** + * Test the behaviour of service stop logic + */ +public class TestServiceStopping extends AbstractServiceLauncherTestBase { + + @Test + public void testStopInStartup() throws Throwable { + FailingStopInStartService svc = new FailingStopInStartService(); + svc.init(new Configuration()); + svc.start(); + assertStopped(svc); + Throwable cause = svc.getFailureCause(); + assertNotNull(cause); + assertTrue(cause instanceof ServiceLaunchException); + assertTrue(svc.waitForServiceToStop(0)); + ServiceLaunchException e = (ServiceLaunchException) cause; + assertEquals(FailingStopInStartService.EXIT_CODE, e.getExitCode()); + } + +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/ExceptionInExecuteLaunchableService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/ExceptionInExecuteLaunchableService.java new file mode 100644 index 0000000..de6377c --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/ExceptionInExecuteLaunchableService.java @@ -0,0 +1,97 @@ +/* + * 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.testservices; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.launcher.AbstractLaunchableService; +import org.apache.hadoop.service.launcher.LauncherExitCodes; +import org.apache.hadoop.service.launcher.ServiceLaunchException; +import org.apache.hadoop.util.ExitCodeProvider; + +import java.io.IOException; +import java.util.List; + +/** + * Raise an exception in the execute() method; the exception type can + * be configured from the CLI + */ +public class ExceptionInExecuteLaunchableService extends + AbstractLaunchableService { + + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.ExceptionInExecuteLaunchableService"; + public static final String ARG_THROW_SLE = "--SLE"; + public static final String ARG_THROW_IOE = "--IOE"; + public static final String ARG_THROWABLE = "--throwable"; + public static final String SLE_TEXT = "SLE raised in execute()"; + public static final String OTHER_EXCEPTION_TEXT = "Other exception"; + + public static final String EXIT_IN_IOE_TEXT = "Exit in IOE"; + public static final int IOE_EXIT_CODE = 64; + ExType exceptionType = ExType.EX; + ; + + public ExceptionInExecuteLaunchableService() { + super("ExceptionInExecuteLaunchedService"); + } + + @Override + public Configuration bindArgs(Configuration config, List args) throws + Exception { + if (args.contains(ARG_THROW_SLE)) { + exceptionType = ExType.SLE; + } else if (args.contains(ARG_THROW_IOE)) { + exceptionType = ExType.IOE; + } else if (args.contains(ARG_THROWABLE)) { + exceptionType = ExType.THROWABLE; + } + return super.bindArgs(config, args); + } + + @Override + public int execute() throws Exception { + switch (exceptionType) { + case SLE: + throw new ServiceLaunchException(LauncherExitCodes.EXIT_OTHER_FAILURE, + SLE_TEXT); + case IOE: + throw new IOECodedException(); + case THROWABLE: + throw new OutOfMemoryError("OOM"); + case EX: + default: + throw new Exception(OTHER_EXCEPTION_TEXT); + } + } + + enum ExType {EX, SLE, IOE, THROWABLE} + + public static class IOECodedException extends IOException implements + ExitCodeProvider { + + public IOECodedException() { + super(EXIT_IN_IOE_TEXT); + } + + @Override + public int getExitCode() { + return IOE_EXIT_CODE; + } + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInConstructorService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInConstructorService.java new file mode 100644 index 0000000..bc51855 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInConstructorService.java @@ -0,0 +1,30 @@ +/* + * 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.testservices; + +public class FailInConstructorService extends FailureTestService { + + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.FailInConstructorService"; + + public FailInConstructorService() { + super(false, false, false, 0); + throw new NullPointerException("oops"); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInInitService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInInitService.java new file mode 100644 index 0000000..705f6a7 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInInitService.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.service.launcher.testservices; + +public class FailInInitService extends FailureTestService { + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.FailInInitService"; + public static final int EXIT_CODE = -1; + + public FailInInitService() { + super(true, false, false, 0 + ); + } + + @Override + int getExitCode() { + return EXIT_CODE; + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInStartService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInStartService.java new file mode 100644 index 0000000..06287af --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailInStartService.java @@ -0,0 +1,34 @@ +/* + * 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.testservices; + +public class FailInStartService extends FailureTestService { + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.FailInStartService"; + public static final int EXIT_CODE = -2; + + public FailInStartService() { + super(false, true, false, 0); + } + + @Override + int getExitCode() { + return EXIT_CODE; + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailingStopInStartService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailingStopInStartService.java new file mode 100644 index 0000000..57a5741 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailingStopInStartService.java @@ -0,0 +1,47 @@ +/* + * 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.testservices; + +/** + * This service also stops immediately + */ +public class FailingStopInStartService extends FailureTestService { + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.FailingStopInStartService"; + public static final int EXIT_CODE = -4; + + public FailingStopInStartService() { + super(false, false, true, 0); + } + + @Override + protected void serviceStart() throws Exception { + super.serviceStart(); + try { + stop(); + } catch (Exception e) { + //this is secretly swallowed + } + } + + @Override + int getExitCode() { + return EXIT_CODE; + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailureTestService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailureTestService.java new file mode 100644 index 0000000..fd70033 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/FailureTestService.java @@ -0,0 +1,55 @@ +/* + * 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.testservices; + +import org.apache.hadoop.service.BreakableService; +import org.apache.hadoop.service.launcher.ServiceLaunchException; + +/** + * Launcher test service that does not take CLI arguments + */ +public class FailureTestService extends BreakableService { + + public final int delay; + + public FailureTestService(boolean failOnInit, + boolean failOnStart, + boolean failOnStop, + int delay) { + super(failOnInit, failOnStart, failOnStop); + this.delay = delay; + } + + @Override + protected void serviceStop() throws Exception { + if (delay > 0) { + Thread.sleep(delay); + } + super.serviceStop(); + } + + @Override + protected Exception createFailureException(String action) { + return new ServiceLaunchException(getExitCode(), toString()); + } + + int getExitCode() { + return -1; + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/InitInConstructorLaunchableService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/InitInConstructorLaunchableService.java new file mode 100644 index 0000000..8374a3e --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/InitInConstructorLaunchableService.java @@ -0,0 +1,63 @@ +/* + * 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.testservices; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.launcher.AbstractLaunchableService; +import org.junit.Assert; + +import java.util.List; + +/** + * Init in the constructor and make sure that it isn't inited again + */ +public class InitInConstructorLaunchableService extends + AbstractLaunchableService { + + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.InitInConstructorLaunchableService"; + private final Configuration originalConf = new Configuration(); + + public InitInConstructorLaunchableService() { + super("InitInConstructorLaunchableService"); + init(originalConf); + } + + @Override + public void init(Configuration conf) { + Assert.assertEquals(STATE.NOTINITED, getServiceState()); + super.init(conf); + } + + @Override + public Configuration bindArgs(Configuration config, List args) throws + Exception { + Assert.assertEquals(STATE.INITED, getServiceState()); + Assert.assertTrue(isInState(STATE.INITED)); + Assert.assertNotSame(getConfig(), config); + return null; + } + + @Override + public int execute() throws Exception { + Assert.assertEquals(STATE.STARTED, getServiceState()); + Assert.assertSame(originalConf, getConfig()); + return super.execute(); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/LaunchableRunningService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/LaunchableRunningService.java new file mode 100644 index 0000000..e571db0 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/LaunchableRunningService.java @@ -0,0 +1,102 @@ +/* + * 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.testservices; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.launcher.LaunchableService; +import org.apache.hadoop.service.launcher.LauncherExitCodes; +import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * A service which implements {@link LaunchableService} and + *
      + *
    1. does nothing in its {@link #serviceStart()}
    2. + *
    3. does its sleep+ maybe fail operation in its {@link #execute()} method
    4. + *
    5. gets the failing flag from the argument {@link #ARG_FAILING} first, + * the config file second.
    6. + *
    7. returns 0 for a succesful execute
    8. + *
    9. returns a configurable exit code for a failing execute
    10. + *
    11. generates a new configuration in its {@link #bindArgs(Configuration, List)} + * to verify that these propagate.
    12. + *
    + */ +public class LaunchableRunningService extends RunningService implements + LaunchableService { + private static final Logger LOG = + LoggerFactory.getLogger(RunningService.class); + 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"; + public int exitCode = 0; + + public LaunchableRunningService() { + this("LaunchableRunningService"); + } + + public LaunchableRunningService(String name) { + super(name); + } + + @Override + public Configuration bindArgs(Configuration config, List args) throws + Exception { + Assert.assertEquals(STATE.NOTINITED, getServiceState()); + for (String arg : args) { + LOG.info(arg); + } + Configuration newConf = new Configuration(config); + if (args.contains(ARG_FAILING)) { + LOG.info("CLI contains " + ARG_FAILING); + failInRun = true; + newConf.setInt(EXIT_CODE_PROP, LauncherExitCodes.EXIT_OTHER_FAILURE); + } + return newConf; + } + + @Override + protected void serviceInit(Configuration conf) throws Exception { + super.serviceInit(conf); + if (conf.getBoolean(FAIL_IN_RUN, false)) { + //if the conf value says fail, the exit code goes to it too + exitCode = LauncherExitCodes.EXIT_FAIL; + } + // the exit code can be read off the property + exitCode = conf.getInt(EXIT_CODE_PROP, exitCode); + } + + @Override + protected void serviceStart() throws Exception { + // no-op + } + + @Override + public int execute() throws Exception { + Thread.sleep(delayTime); + if (failInRun) { + return exitCode; + } + return 0; + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NoArgsAllowedService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NoArgsAllowedService.java new file mode 100644 index 0000000..301d4f5 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NoArgsAllowedService.java @@ -0,0 +1,63 @@ +/* + * 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.testservices; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.launcher.AbstractLaunchableService; +import org.apache.hadoop.service.launcher.LauncherExitCodes; +import org.apache.hadoop.service.launcher.ServiceLaunchException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * service that does not allow any arguments + */ +public class NoArgsAllowedService extends AbstractLaunchableService { + + private static final Logger LOG = + LoggerFactory.getLogger(NoArgsAllowedService.class); + + public NoArgsAllowedService() { + super("NoArgsAllowedService"); + } + + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.NoArgsAllowedService"; + + @Override + public Configuration bindArgs(Configuration config, List args) throws + Exception { + Configuration configuration = super.bindArgs(config, args); + if (!args.isEmpty()) { + StringBuilder argsList = new StringBuilder(); + for (String arg : args) { + argsList.append('"').append(arg).append("\" "); + } + LOG.error("Got {} arguments: {}", args.size(), argsList); + throw new ServiceLaunchException(LauncherExitCodes.EXIT_COMMAND_ARGUMENT_ERROR, + "Expected 0 arguments but got %d: %s", + args.size(), + argsList); + } + return configuration; + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NullBindLaunchableService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NullBindLaunchableService.java new file mode 100644 index 0000000..1f1c448 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NullBindLaunchableService.java @@ -0,0 +1,46 @@ +/* + * 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.testservices; + +import org.apache.hadoop.conf.Configuration; + +import java.util.List; + +/** + * An extension of {@link LaunchableRunningService} which returns null from + * the {@link #bindArgs(Configuration, List)} method. + */ +public class NullBindLaunchableService extends LaunchableRunningService { + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.NullBindLaunchableService"; + + public NullBindLaunchableService() { + this("NullBindLaunchableService"); + } + + public NullBindLaunchableService(String name) { + super(name); + } + + @Override + public Configuration bindArgs(Configuration config, List args) throws + Exception { + return null; + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/RunningService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/RunningService.java new file mode 100644 index 0000000..93187ed --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/RunningService.java @@ -0,0 +1,79 @@ +/* + * 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.testservices; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.service.AbstractService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RunningService extends AbstractService implements Runnable { + private static final Logger LOG = + LoggerFactory.getLogger(RunningService.class); + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.RunningService"; + public static final int DELAY = 100; + + /** + * Property on delay times + */ + public static final String DELAY_TIME = "delay.time"; + public static final String FAIL_IN_RUN = "fail.runnable"; + public static final String FAILURE_MESSAGE = "FAIL_IN_RUN"; + public boolean interrupted; + + public int delayTime = DELAY; + public boolean failInRun; + + public RunningService() { + super("RunningService"); + } + + public RunningService(String name) { + super(name); + } + + @Override + protected void serviceInit(Configuration conf) throws Exception { + super.serviceInit(conf); + delayTime = getConfig().getInt(DELAY_TIME, delayTime); + failInRun = getConfig().getBoolean(FAIL_IN_RUN, failInRun); + } + + @Override + protected void serviceStart() throws Exception { + Thread thread = new Thread(this); + thread.setName(getName()); + thread.start(); + } + + @Override + public void run() { + try { + Thread.sleep(delayTime); + if (failInRun) { + noteFailure(new Exception(FAILURE_MESSAGE)); + } + } catch (InterruptedException e) { + interrupted = true; + LOG.info("Interrupted"); + } + stop(); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/StoppingInStartLaunchableService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/StoppingInStartLaunchableService.java new file mode 100644 index 0000000..41e1549 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/StoppingInStartLaunchableService.java @@ -0,0 +1,45 @@ +/* + * 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.testservices; + +import org.apache.hadoop.service.launcher.AbstractLaunchableService; +import org.apache.hadoop.service.launcher.LauncherExitCodes; +import org.apache.hadoop.service.launcher.ServiceLaunchException; + +public class StoppingInStartLaunchableService extends AbstractLaunchableService { + + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.StoppingInStartLaunchableService"; + public StoppingInStartLaunchableService(String name) { + super(name); + } + + @Override + protected void serviceStart() throws Exception { + super.serviceStart(); + stop(); + } + + @Override + public int execute() throws Exception { + throw new ServiceLaunchException( + LauncherExitCodes.EXIT_SERVICE_LIFECYCLE_EXCEPTION, + "Should not have been executed"); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/StringConstructorOnlyService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/StringConstructorOnlyService.java new file mode 100644 index 0000000..4a31bbb --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/StringConstructorOnlyService.java @@ -0,0 +1,39 @@ +/* + * 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.testservices; + +import org.apache.hadoop.service.launcher.AbstractLaunchableService; + +/** + * service that only has one constructor that takes a string. + * This is the standard base class of a YARN service, so handle it + * in the launch + */ +public class StringConstructorOnlyService extends AbstractLaunchableService { + + + public StringConstructorOnlyService(String name) { + super(name); + } + + public static final String NAME = + "org.apache.hadoop.service.launcher.testservices.StringConstructorOnlyService"; + + +}