// Copyright 2016 Twitter. All rights reserved. // // Licensed 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 com.twitter.heron.scheduler; import java.io.PrintStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import com.twitter.heron.api.generated.TopologyAPI; import com.twitter.heron.common.basics.DryRunFormatType; import com.twitter.heron.common.basics.PackageType; import com.twitter.heron.common.basics.SysUtils; import com.twitter.heron.common.utils.logging.LoggingHelper; import com.twitter.heron.scheduler.dryrun.SubmitDryRunResponse; import com.twitter.heron.scheduler.dryrun.SubmitRawDryRunRenderer; import com.twitter.heron.scheduler.dryrun.SubmitTableDryRunRenderer; import com.twitter.heron.scheduler.utils.LauncherUtils; import com.twitter.heron.spi.common.Config; import com.twitter.heron.spi.common.ConfigLoader; import com.twitter.heron.spi.common.Context; import com.twitter.heron.spi.common.Key; import com.twitter.heron.spi.packing.PackingException; import com.twitter.heron.spi.packing.PackingPlan; import com.twitter.heron.spi.scheduler.ILauncher; import com.twitter.heron.spi.scheduler.LauncherException; import com.twitter.heron.spi.statemgr.IStateManager; import com.twitter.heron.spi.statemgr.SchedulerStateManagerAdaptor; import com.twitter.heron.spi.uploader.IUploader; import com.twitter.heron.spi.uploader.UploaderException; import com.twitter.heron.spi.utils.ReflectionUtils; import com.twitter.heron.spi.utils.TopologyUtils; /** * Calls Uploader to upload topology package, and Launcher to launch Scheduler. */ public class SubmitterMain { private static final Logger LOG = Logger.getLogger(SubmitterMain.class.getName()); /** * Load the topology config * * @param topologyPackage, tar ball containing user submitted jar/tar, defn and config * @param topologyBinaryFile, name of the user submitted topology jar/tar/pex file * @param topology, proto in memory version of topology definition * @return config, the topology config */ protected static Config topologyConfigs( String topologyPackage, String topologyBinaryFile, String topologyDefnFile, TopologyAPI.Topology topology) { PackageType packageType = PackageType.getPackageType(topologyBinaryFile); return Config.newBuilder() .put(Key.TOPOLOGY_ID, topology.getId()) .put(Key.TOPOLOGY_NAME, topology.getName()) .put(Key.TOPOLOGY_DEFINITION_FILE, topologyDefnFile) .put(Key.TOPOLOGY_PACKAGE_FILE, topologyPackage) .put(Key.TOPOLOGY_BINARY_FILE, topologyBinaryFile) .put(Key.TOPOLOGY_PACKAGE_TYPE, packageType) .build(); } /** * Load the config parameters from the command line * * @param cluster, name of the cluster * @param role, user role * @param environ, user provided environment/tag * @param verbose, enable verbose logging * @return config, the command line config */ protected static Config commandLineConfigs(String cluster, String role, String environ, Boolean dryRun, DryRunFormatType dryRunFormat, Boolean verbose) { return Config.newBuilder() .put(Key.CLUSTER, cluster) .put(Key.ROLE, role) .put(Key.ENVIRON, environ) .put(Key.DRY_RUN, dryRun) .put(Key.DRY_RUN_FORMAT_TYPE, dryRunFormat) .put(Key.VERBOSE, verbose) .build(); } // Print usage options private static void usage(Options options) { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("SubmitterMain", options); } // Construct all required command line options private static Options constructOptions() { Options options = new Options(); Option cluster = Option.builder("c") .desc("Cluster name in which the topology needs to run on") .longOpt("cluster") .hasArgs() .argName("cluster") .required() .build(); Option role = Option.builder("r") .desc("Role under which the topology needs to run") .longOpt("role") .hasArgs() .argName("role") .required() .build(); Option environment = Option.builder("e") .desc("Environment under which the topology needs to run") .longOpt("environment") .hasArgs() .argName("environment") .required() .build(); Option heronHome = Option.builder("d") .desc("Directory where heron is installed") .longOpt("heron_home") .hasArgs() .argName("heron home dir") .required() .build(); Option configFile = Option.builder("p") .desc("Path of the config files") .longOpt("config_path") .hasArgs() .argName("config path") .required() .build(); Option configOverrides = Option.builder("o") .desc("Command line override config path") .longOpt("override_config_file") .hasArgs() .argName("override config file") .build(); Option releaseFile = Option.builder("b") .desc("Release file name") .longOpt("release_file") .hasArgs() .argName("release information") .build(); Option topologyPackage = Option.builder("y") .desc("tar ball containing user submitted jar/tar, defn and config") .longOpt("topology_package") .hasArgs() .argName("topology package") .required() .build(); Option topologyDefn = Option.builder("f") .desc("serialized file containing Topology protobuf") .longOpt("topology_defn") .hasArgs() .argName("topology definition") .required() .build(); Option topologyJar = Option.builder("j") .desc("user heron topology jar/pex file path") .longOpt("topology_bin") .hasArgs() .argName("topology binary file") .required() .build(); Option dryRun = Option.builder("u") .desc("run in dry-run mode") .longOpt("dry_run") .required(false) .build(); Option dryRunFormat = Option.builder("t") .desc("dry-run format") .longOpt("dry_run_format") .hasArg() .required(false) .build(); Option verbose = Option.builder("v") .desc("Enable debug logs") .longOpt("verbose") .build(); options.addOption(cluster); options.addOption(role); options.addOption(environment); options.addOption(heronHome); options.addOption(configFile); options.addOption(configOverrides); options.addOption(releaseFile); options.addOption(topologyPackage); options.addOption(topologyDefn); options.addOption(topologyJar); options.addOption(dryRun); options.addOption(dryRunFormat); options.addOption(verbose); return options; } // construct command line help options private static Options constructHelpOptions() { Options options = new Options(); Option help = Option.builder("h") .desc("List all options and their description") .longOpt("help") .build(); options.addOption(help); return options; } private static boolean isVerbose(CommandLine cmd) { return cmd.hasOption("v"); } @VisibleForTesting public static Config loadConfig(CommandLine cmd, TopologyAPI.Topology topology) { String cluster = cmd.getOptionValue("cluster"); String role = cmd.getOptionValue("role"); String environ = cmd.getOptionValue("environment"); String heronHome = cmd.getOptionValue("heron_home"); String configPath = cmd.getOptionValue("config_path"); String overrideConfigFile = cmd.getOptionValue("override_config_file"); String releaseFile = cmd.getOptionValue("release_file"); String topologyPackage = cmd.getOptionValue("topology_package"); String topologyDefnFile = cmd.getOptionValue("topology_defn"); String topologyBinaryFile = cmd.getOptionValue("topology_bin"); Boolean dryRun = false; if (cmd.hasOption("u")) { dryRun = true; } // Default dry-run output format type DryRunFormatType dryRunFormat = DryRunFormatType.TABLE; if (dryRun && cmd.hasOption("t")) { String format = cmd.getOptionValue("dry_run_format"); dryRunFormat = DryRunFormatType.getDryRunFormatType(format); LOG.fine(String.format("Running dry-run mode using format %s", format)); } // first load the defaults, then the config from files to override it // next add config parameters from the command line // load the topology configs // build the final config by expanding all the variables return Config.toLocalMode(Config.newBuilder() .putAll(ConfigLoader.loadConfig(heronHome, configPath, releaseFile, overrideConfigFile)) .putAll(commandLineConfigs(cluster, role, environ, dryRun, dryRunFormat, isVerbose(cmd))) .putAll(topologyConfigs(topologyPackage, topologyBinaryFile, topologyDefnFile, topology)) .build()); } public static void main(String[] args) throws Exception { Options options = constructOptions(); Options helpOptions = constructHelpOptions(); CommandLineParser parser = new DefaultParser(); // parse the help options first. CommandLine cmd = parser.parse(helpOptions, args, true); if (cmd.hasOption("h")) { usage(options); return; } try { // Now parse the required options cmd = parser.parse(options, args); } catch (ParseException e) { usage(options); throw new RuntimeException("Error parsing command line options: ", e); } Level logLevel = Level.INFO; if (isVerbose(cmd)) { logLevel = Level.ALL; } // init log LoggingHelper.loggerInit(logLevel, false); // load the topology definition into topology proto TopologyAPI.Topology topology = TopologyUtils.getTopology(cmd.getOptionValue("topology_defn")); Config config = loadConfig(cmd, topology); LOG.fine("Static config loaded successfully"); LOG.fine(config.toString()); SubmitterMain submitterMain = new SubmitterMain(config, topology); /* Meaning of exit status code: - status code = 0: program exits without error - 0 < status code < 100: program fails to execute before program execution. For example, JVM cannot find or load main class - 100 <= status code < 200: program fails to launch after program execution. For example, topology definition file fails to be loaded - status code >= 200 program sends out dry-run response */ try { submitterMain.submitTopology(); } catch (SubmitDryRunResponse response) { LOG.log(Level.FINE, "Sending out dry-run response"); // Output may contain UTF-8 characters, so we should print using UTF-8 encoding PrintStream out = new PrintStream(System.out, true, StandardCharsets.UTF_8.name()); out.print(submitterMain.renderDryRunResponse(response)); // Exit with status code 200 to indicate dry-run response is sent out // SUPPRESS CHECKSTYLE RegexpSinglelineJava System.exit(200); // SUPPRESS CHECKSTYLE IllegalCatch } catch (Exception e) { /* Since only stderr is used (by logging), we use stdout here to propagate error message back to Python's executor.py (invoke site). */ LOG.log(Level.FINE, "Exception when submitting topology", e); System.out.println(e.getMessage()); // Exit with status code 100 to indicate that error has happened on user-land // SUPPRESS CHECKSTYLE RegexpSinglelineJava System.exit(100); } LOG.log(Level.FINE, "Topology {0} submitted successfully", topology.getName()); } // holds all the config read private final Config config; // topology definition private final TopologyAPI.Topology topology; public SubmitterMain(Config config, TopologyAPI.Topology topology) { // initialize the options this.config = config; this.topology = topology; } /** * Submit a topology * 1. Instantiate necessary resources * 2. Valid whether it is legal to submit a topology * 3. Call LauncherRunner * */ public void submitTopology() throws TopologySubmissionException { // build primary runtime config first Config primaryRuntime = Config.newBuilder() .putAll(LauncherUtils.getInstance().createPrimaryRuntime(topology)).build(); // call launcher directly here if in dry-run mode if (Context.dryRun(config)) { callLauncherRunner(primaryRuntime); return; } // 1. Do prepare work // create an instance of state manager String statemgrClass = Context.stateManagerClass(config); IStateManager statemgr; // Create an instance of the launcher class String launcherClass = Context.launcherClass(config); ILauncher launcher; // create an instance of the uploader class String uploaderClass = Context.uploaderClass(config); IUploader uploader; // create an instance of state manager try { statemgr = ReflectionUtils.newInstance(statemgrClass); } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) { throw new TopologySubmissionException( String.format("Failed to instantiate state manager class '%s'", statemgrClass), e); } // create an instance of launcher try { launcher = ReflectionUtils.newInstance(launcherClass); } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) { throw new LauncherException( String.format("Failed to instantiate launcher class '%s'", launcherClass), e); } // create an instance of uploader try { uploader = ReflectionUtils.newInstance(uploaderClass); } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) { throw new UploaderException( String.format("Failed to instantiate uploader class '%s'", uploaderClass), e); } // Put it in a try block so that we can always clean resources try { // initialize the state manager statemgr.initialize(config); // TODO(mfu): timeout should read from config SchedulerStateManagerAdaptor adaptor = new SchedulerStateManagerAdaptor(statemgr, 5000); // Check if topology is already running validateSubmit(adaptor, topology.getName()); LOG.log(Level.FINE, "Topology {0} to be submitted", topology.getName()); Config runtimeWithoutPackageURI = Config.newBuilder() .putAll(primaryRuntime) .putAll(LauncherUtils.getInstance().createAdaptorRuntime(adaptor)) .put(Key.LAUNCHER_CLASS_INSTANCE, launcher) .build(); PackingPlan packingPlan = LauncherUtils.getInstance() .createPackingPlan(config, runtimeWithoutPackageURI); // The packing plan might call for a number of containers different than the config // settings. If that's the case we need to modify the configs to match. runtimeWithoutPackageURI = updateNumContainersIfNeeded(runtimeWithoutPackageURI, topology, packingPlan); // If the packing plan is valid we will upload necessary packages URI packageURI = uploadPackage(uploader); // Update the runtime config with the packageURI Config runtimeAll = Config.newBuilder() .putAll(runtimeWithoutPackageURI) .put(Key.TOPOLOGY_PACKAGE_URI, packageURI) .build(); callLauncherRunner(runtimeAll); } catch (LauncherException | PackingException e) { // we undo uploading of topology package only if launcher fails to // launch topology, which will throw LauncherException or PackingException uploader.undo(); throw e; } finally { SysUtils.closeIgnoringExceptions(uploader); SysUtils.closeIgnoringExceptions(launcher); SysUtils.closeIgnoringExceptions(statemgr); } } /** * Checks that the number of containers specified in the topology matches the number of containers * called for in the packing plan. If they are different, returns a new config with settings * updated to align with the packing plan. The new config will include an updated * Key.TOPOLOGY_DEFINITION containing a cloned Topology with it's settings also updated. * * @param initialConfig initial config to clone and update (if necessary) * @param initialTopology topology to check and clone/update (if necessary) * @param packingPlan packing plan to compare settings with * @return a new Config cloned from initialConfig and modified as needed to align with packedPlan */ @VisibleForTesting Config updateNumContainersIfNeeded(Config initialConfig, TopologyAPI.Topology initialTopology, PackingPlan packingPlan) { int configNumStreamManagers = TopologyUtils.getNumContainers(initialTopology); int packingNumStreamManagers = packingPlan.getContainers().size(); if (configNumStreamManagers == packingNumStreamManagers) { return initialConfig; } Config.Builder newConfigBuilder = Config.newBuilder() .putAll(initialConfig) .put(Key.NUM_CONTAINERS, packingNumStreamManagers + 1) .put(Key.TOPOLOGY_DEFINITION, cloneWithNewNumContainers(initialTopology, packingNumStreamManagers)); String packingClass = Context.packingClass(initialConfig); LOG.warning(String.format("The packing plan (generated by %s) calls for a different number of " + "containers (%d) than what was explicitly set in the topology configs (%d). " + "Overriding the configs to specify %d containers. When using %s do not explicitly " + "call config.setNumStmgrs(..) or config.setNumWorkers(..).", packingClass, packingNumStreamManagers, configNumStreamManagers, packingNumStreamManagers, packingClass)); return newConfigBuilder.build(); } private TopologyAPI.Topology cloneWithNewNumContainers(TopologyAPI.Topology initialTopology, int numStreamManagers) { TopologyAPI.Topology.Builder topologyBuilder = TopologyAPI.Topology.newBuilder(initialTopology); TopologyAPI.Config.Builder configBuilder = TopologyAPI.Config.newBuilder(); for (TopologyAPI.Config.KeyValue keyValue : initialTopology.getTopologyConfig().getKvsList()) { // override TOPOLOGY_STMGRS value once we find it if (com.twitter.heron.api.Config.TOPOLOGY_STMGRS.equals(keyValue.getKey())) { TopologyAPI.Config.KeyValue.Builder kvBuilder = TopologyAPI.Config.KeyValue.newBuilder(); kvBuilder.setKey(keyValue.getKey()); kvBuilder.setValue(Integer.toString(numStreamManagers)); configBuilder.addKvs(kvBuilder.build()); } else { configBuilder.addKvs(keyValue); } } return topologyBuilder.setTopologyConfig(configBuilder).build(); } protected void validateSubmit(SchedulerStateManagerAdaptor adaptor, String topologyName) throws TopologySubmissionException { // Check whether the topology has already been running // TODO(rli): anti-pattern is too nested on this path to be refactored Boolean isTopologyRunning = adaptor.isTopologyRunning(topologyName); if (isTopologyRunning != null && isTopologyRunning.equals(Boolean.TRUE)) { throw new TopologySubmissionException( String.format("Topology '%s' already exists", topologyName)); } } protected URI uploadPackage(IUploader uploader) throws UploaderException { // initialize the uploader uploader.initialize(config); // upload the topology package to the storage return uploader.uploadPackage(); } protected void callLauncherRunner(Config runtime) throws LauncherException, PackingException, SubmitDryRunResponse { // using launch runner, launch the topology LaunchRunner launchRunner = new LaunchRunner(config, runtime); launchRunner.call(); } protected String renderDryRunResponse(SubmitDryRunResponse resp) { DryRunFormatType formatType = Context.dryRunFormatType(config); switch (formatType) { case RAW : return new SubmitRawDryRunRenderer(resp).render(); case TABLE: return new SubmitTableDryRunRenderer(resp, false).render(); case COLORED_TABLE: return new SubmitTableDryRunRenderer(resp, true).render(); default: throw new IllegalArgumentException( String.format("Unexpected rendering format: %s", formatType)); } } }