Java tutorial
/* * 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.twill.internal; import com.google.common.base.Charsets; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.Uninterruptibles; import com.google.gson.Gson; import com.google.gson.JsonElement; import org.apache.twill.api.RunId; import org.apache.twill.api.RuntimeSpecification; import org.apache.twill.filesystem.Location; import org.apache.twill.internal.state.Message; import org.apache.twill.internal.utils.Resources; import org.apache.twill.launcher.FindFreePort; import org.apache.twill.launcher.TwillLauncher; import org.apache.twill.zookeeper.NodeData; import org.apache.twill.zookeeper.ZKClient; import org.apache.twill.zookeeper.ZKOperations; import org.apache.zookeeper.KeeperException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * This class helps launching a container. */ public final class TwillContainerLauncher { private static final Logger LOG = LoggerFactory.getLogger(TwillContainerLauncher.class); private final RuntimeSpecification runtimeSpec; private final ContainerInfo containerInfo; private final ProcessLauncher.PrepareLaunchContext launchContext; private final ZKClient zkClient; private final int instanceCount; private final JvmOptions jvmOpts; private final int reservedMemory; private final double minHeapRatio; private final Location secureStoreLocation; private int maxHeapSizeMB; public TwillContainerLauncher(RuntimeSpecification runtimeSpec, ContainerInfo containerInfo, ProcessLauncher.PrepareLaunchContext launchContext, ZKClient zkClient, int instanceCount, JvmOptions jvmOpts, int reservedMemory, double minHeapRatio, Location secureStoreLocation) { this.runtimeSpec = runtimeSpec; this.containerInfo = containerInfo; this.launchContext = launchContext; this.zkClient = zkClient; this.instanceCount = instanceCount; this.jvmOpts = jvmOpts; this.reservedMemory = reservedMemory; this.minHeapRatio = minHeapRatio; this.secureStoreLocation = secureStoreLocation; } /** * Start execution run for a class in a container. Will return instance of {@link TwillContainerController} * that allows sending messages directly to the container. * * @param runId Use to represent unique id of the container run. * @param instanceId The Twill instance Id. * @param mainClass The main class to run in the container. * @param classPath The class path to load classes for the container. * @param logLevelLocation The log level file location for the container to localize. * @return instance of {@link TwillContainerController} to control the container run. */ public TwillContainerController start(RunId runId, int instanceId, Class<?> mainClass, String classPath, @Nullable Location logLevelLocation) { // Clean up zookeeper path in case this is a retry and there are old messages and state there. Futures.getUnchecked(ZKOperations.ignoreError(ZKOperations.recursiveDelete(zkClient, "/" + runId), KeeperException.NoNodeException.class, null)); // Adds all file to be localized to container launchContext.addResources(runtimeSpec.getLocalFiles()); // Optionally localize secure store and log level file. try { if (secureStoreLocation != null && secureStoreLocation.exists()) { launchContext .addResources(new DefaultLocalFile(Constants.Files.CREDENTIALS, secureStoreLocation.toURI(), secureStoreLocation.lastModified(), secureStoreLocation.length(), false, null)); } if (logLevelLocation != null && logLevelLocation.exists()) { launchContext .addResources(new DefaultLocalFile(Constants.Files.LOG_LEVELS, logLevelLocation.toURI(), logLevelLocation.lastModified(), logLevelLocation.length(), false, null)); } } catch (IOException e) { LOG.warn("Failed to launch container with secure store {}.", secureStoreLocation); } // Currently no reporting is supported for runnable containers launchContext.addEnvironment(EnvKeys.TWILL_RUN_ID, runId.getId()) .addEnvironment(EnvKeys.TWILL_RUNNABLE_NAME, runtimeSpec.getName()) .addEnvironment(EnvKeys.TWILL_INSTANCE_ID, Integer.toString(instanceId)) .addEnvironment(EnvKeys.TWILL_INSTANCE_COUNT, Integer.toString(instanceCount)); // assemble the command based on jvm options ImmutableList.Builder<String> commandBuilder = ImmutableList.builder(); String firstCommand; if (jvmOpts.getDebugOptions().doDebug(runtimeSpec.getName())) { // for debugging we run a quick Java program to find a free port, then pass that port as the debug port and also // as a System property to the runnable (Java has no general way to determine the port from within the JVM). // PORT=$(java FindFreePort) && java -agentlib:jdwp=...,address=\$PORT -Dtwill.debug.port=\$PORT... TwillLauncher // The $ must be escaped, otherwise it gets expanded (to "") before the command is submitted. String suspend = jvmOpts.getDebugOptions().doSuspend() ? "y" : "n"; firstCommand = "TWILL_DEBUG_PORT=$($JAVA_HOME/bin/java"; commandBuilder.add("-cp", Constants.Files.LAUNCHER_JAR, FindFreePort.class.getName() + ")", "&&", // this will stop if FindFreePort fails "$JAVA_HOME/bin/java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=" + suspend + "," + "address=\\$TWILL_DEBUG_PORT", "-Dtwill.debug.port=\\$TWILL_DEBUG_PORT"); } else { firstCommand = "$JAVA_HOME/bin/java"; } maxHeapSizeMB = Resources.computeMaxHeapSize(containerInfo.getMemoryMB(), reservedMemory, minHeapRatio); commandBuilder.add("-Djava.io.tmpdir=tmp", "-Dyarn.container=$" + EnvKeys.YARN_CONTAINER_ID, "-Dtwill.runnable=$" + Constants.TWILL_APP_NAME + ".$" + EnvKeys.TWILL_RUNNABLE_NAME, "-cp", Constants.Files.LAUNCHER_JAR + ":" + classPath, "-Xmx" + maxHeapSizeMB + "m"); String extraOptions = jvmOpts.getRunnableExtraOptions(runtimeSpec.getName()); if (!extraOptions.isEmpty()) { commandBuilder.add(extraOptions); } commandBuilder.add(TwillLauncher.class.getName(), mainClass.getName(), Boolean.TRUE.toString()); List<String> command = commandBuilder.build(); ProcessController<Void> processController = launchContext .addCommand(firstCommand, command.toArray(new String[command.size()])).launch(); TwillContainerControllerImpl controller = new TwillContainerControllerImpl(zkClient, runId, runtimeSpec.getName(), instanceId, processController); controller.start(); return controller; } /** * Returns the maximum heap memory size in MB of the Java process launched in the container. * This method can only be called after the {@link #start(RunId, int, Class, String, Location)} method. * * @throws IllegalStateException if the {@link #start(RunId, int, Class, String, Location)} was not called yet. */ public int getMaxHeapMemoryMB() { if (maxHeapSizeMB <= 0) { throw new IllegalStateException( "Unknown maximum heap memory size. Please make sure the container is started"); } return maxHeapSizeMB; } private static final class TwillContainerControllerImpl extends AbstractZKServiceController implements TwillContainerController { private final String runnable; private final int instanceId; private final ProcessController<Void> processController; // This latch can be used to wait for container shutdown private final CountDownLatch shutdownLatch; private volatile ContainerLiveNodeData liveData; protected TwillContainerControllerImpl(ZKClient zkClient, RunId runId, String runnable, int instanceId, ProcessController<Void> processController) { super(runId, zkClient); this.runnable = runnable; this.instanceId = instanceId; this.processController = processController; this.shutdownLatch = new CountDownLatch(1); } @Override protected void doStartUp() { // No-op } @Override protected void doShutDown() { // Wait for sometime for the container to stop // TODO: Use configurable value for stop time (TWILL-192) int maxWaitSecs = Constants.APPLICATION_MAX_STOP_SECONDS - 30; maxWaitSecs = maxWaitSecs < 15 ? 15 : maxWaitSecs; try { if (Uninterruptibles.awaitUninterruptibly(shutdownLatch, maxWaitSecs, TimeUnit.SECONDS)) { return; } } catch (Exception e) { LOG.error("Got exception while trying to stop runnable {}, instance {}", runnable, instanceId, e); } // Container has not shutdown even after maxWaitSecs after sending stop message, // we'll need to kill the container LOG.warn("Killing runnable {}, instance {} after waiting {} secs", runnable, instanceId, maxWaitSecs); killAndWait(maxWaitSecs); } @Override protected void instanceNodeUpdated(NodeData nodeData) { if (nodeData == null || nodeData.getData() == null) { LOG.warn("Instance node was updated but data is null."); return; } try { Gson gson = new Gson(); JsonElement json = gson.fromJson(new String(nodeData.getData(), Charsets.UTF_8), JsonElement.class); if (json.isJsonObject()) { JsonElement data = json.getAsJsonObject().get("data"); if (data != null) { this.liveData = gson.fromJson(data, ContainerLiveNodeData.class); LOG.info("Container LiveNodeData updated: " + new String(nodeData.getData(), Charsets.UTF_8)); } } } catch (Throwable t) { LOG.warn("Error deserializing updated instance node data", t); } } @Override protected void instanceNodeFailed(Throwable cause) { // No-op } @Override public ListenableFuture<Message> sendMessage(Message message) { return sendMessage(message, message); } @Override public void completed(int exitStatus) { // count down the shutdownLatch to inform any waiting threads that this container is complete if (exitStatus == 0) { setTerminationStatus(TerminationStatus.SUCCEEDED); } else if (exitStatus == 143) { setTerminationStatus(TerminationStatus.KILLED); } else { setTerminationStatus(TerminationStatus.FAILED); } shutdownLatch.countDown(); synchronized (this) { forceShutDown(); } } @Override public ContainerLiveNodeData getLiveNodeData() { return liveData; } @Override public void kill() { processController.cancel(); } @Override public int getInstanceId() { return instanceId; } private void killAndWait(int maxWaitSecs) { Stopwatch watch = new Stopwatch(); watch.start(); while (watch.elapsedTime(TimeUnit.SECONDS) < maxWaitSecs) { // Kill the application try { kill(); } catch (Exception e) { LOG.error("Exception while killing runnable {}, instance {}", runnable, instanceId, e); } // Wait on the shutdownLatch, // if the runnable has stopped then the latch will be count down by completed() method if (Uninterruptibles.awaitUninterruptibly(shutdownLatch, 10, TimeUnit.SECONDS)) { // Runnable has stopped now return; } } // Timeout reached, runnable has not stopped LOG.error("Failed to kill runnable {}, instance {} after {} seconds", runnable, instanceId, watch.elapsedTime(TimeUnit.SECONDS)); // TODO: should we throw exception here? } } }