// 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.marathon; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.logging.Logger; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Joiner; import com.twitter.heron.common.basics.FileUtils; import com.twitter.heron.proto.scheduler.Scheduler; import com.twitter.heron.scheduler.utils.Runtime; import com.twitter.heron.scheduler.utils.SchedulerUtils; import com.twitter.heron.spi.common.Config; import com.twitter.heron.spi.common.Context; import com.twitter.heron.spi.common.Key; import com.twitter.heron.spi.packing.PackingPlan; import com.twitter.heron.spi.packing.Resource; import com.twitter.heron.spi.scheduler.IScheduler; public class MarathonScheduler implements IScheduler { private static final Logger LOG = Logger.getLogger(MarathonScheduler.class.getName()); private Config config; private Config runtime; private MarathonController controller; @Override public void initialize(Config aConfig, Config aRuntime) { this.config = aConfig; this.runtime = aRuntime; this.controller = getController(); } protected MarathonController getController() { return new MarathonController( MarathonContext.getSchedulerURI(config), Runtime.topologyName(runtime), Context.verbose(config)); } @Override public void close() { // Do nothing } @Override public boolean onSchedule(PackingPlan packing) { if (packing == null || packing.getContainers().isEmpty()) { LOG.severe("No container requested. Can't schedule"); return false; } LOG.info("Submitting topology to Marathon Scheduler"); String topologyConf = getTopologyConf(packing); return controller.submitTopology(topologyConf); } @Override public boolean onUpdate(Scheduler.UpdateTopologyRequest request) { LOG.severe("Topology onUpdate not implemented by this scheduler."); return false; } @Override public List<String> getJobLinks() { List<String> jobLinks = new LinkedList<>(); String marathonGroupLink = MarathonContext.getSchedulerURI(config) + MarathonConstants.JOB_LINK + Runtime.topologyName(runtime); jobLinks.add(marathonGroupLink); return jobLinks; } @Override public boolean onKill(Scheduler.KillTopologyRequest request) { return controller.killTopology(); } @Override public boolean onRestart(Scheduler.RestartTopologyRequest request) { int appId = request.getContainerIndex(); return controller.restartApp(appId); } protected String getTopologyConf(PackingPlan packing) { config = Config.newBuilder() .putAll(config) .put(Key.TOPOLOGY_BINARY_FILE, FileUtils.getBaseName(Context.topologyBinaryFile(config))) .build(); ObjectMapper mapper = new ObjectMapper(); // TODO (nlu): use heterogeneous resources // Align resources to maximal requested resource PackingPlan updatedPackingPlan = packing.cloneWithHomogeneousScheduledResource(); SchedulerUtils.persistUpdatedPackingPlan(Runtime.topologyName(runtime), updatedPackingPlan, Runtime.schedulerStateManagerAdaptor(runtime)); Resource containerResource = updatedPackingPlan.getContainers() .iterator().next().getRequiredResource(); // Create app conf list for each container ArrayNode instances = mapper.createArrayNode(); for (int i = 0; i < Runtime.numContainers(runtime); i++) { ObjectNode instance = mapper.createObjectNode(); instance.put(MarathonConstants.ID, Integer.toString(i)); instance.put(MarathonConstants.COMMAND, getExecutorCommand(i)); instance.put(MarathonConstants.CPU, containerResource.getCpu()); instance.set(MarathonConstants.CONTAINER, getContainer(mapper)); instance.put(MarathonConstants.MEMORY, containerResource.getRam().asMegabytes()); instance.put(MarathonConstants.DISK, containerResource.getDisk().asMegabytes()); instance.put(MarathonConstants.INSTANCES, 1); instance.set(MarathonConstants.LABELS, getLabels(mapper)); instance.set(MarathonConstants.FETCH, getFetchList(mapper)); instances.add(instance); } // Create marathon group for a topology ObjectNode appConf = mapper.createObjectNode(); appConf.put(MarathonConstants.ID, Runtime.topologyName(runtime)); appConf.set(MarathonConstants.APPS, instances); return appConf.toString(); } // build the container object protected ObjectNode getContainer(ObjectMapper mapper) { ObjectNode containerNode = mapper.createObjectNode(); containerNode.put(MarathonConstants.CONTAINER_TYPE, "DOCKER"); containerNode.set("docker", getDockerContainer(mapper)); return containerNode; } protected ObjectNode getDockerContainer(ObjectMapper mapper) { ObjectNode dockerNode = mapper.createObjectNode(); dockerNode.put(MarathonConstants.DOCKER_IMAGE, MarathonContext.getExecutorDockerImage(config)); dockerNode.put(MarathonConstants.DOCKER_NETWORK, MarathonConstants.DOCKER_NETWORK_BRIDGE); dockerNode.put(MarathonConstants.DOCKER_PRIVILEGED, false); dockerNode.put(MarathonConstants.DOCKER_FORCE_PULL, true); dockerNode.set(MarathonConstants.DOCKER_PORT_MAPPINGS, getPorts(mapper)); return dockerNode; } protected ObjectNode getLabels(ObjectMapper mapper) { ObjectNode labelNode = mapper.createObjectNode(); labelNode.put(MarathonConstants.ENVIRONMENT, Context.environ(config)); return labelNode; } protected ArrayNode getFetchList(ObjectMapper mapper) { String heronCoreURI = Context.corePackageUri(config); String topologyURI = Runtime.topologyPackageUri(runtime).toString(); String[] uris = new String[]{heronCoreURI, topologyURI}; ArrayNode urisNode = mapper.createArrayNode(); for (String uri : uris) { ObjectNode uriObject = mapper.createObjectNode(); uriObject.put(MarathonConstants.URI, uri); uriObject.put(MarathonConstants.EXECUTABLE, false); uriObject.put(MarathonConstants.EXTRACT, true); uriObject.put(MarathonConstants.CACHE, false); urisNode.add(uriObject); } return urisNode; } protected ArrayNode getPorts(ObjectMapper mapper) { ArrayNode ports = mapper.createArrayNode(); for (String portName : MarathonConstants.PORT_NAMES) { ObjectNode port = mapper.createObjectNode(); port.put(MarathonConstants.DOCKER_CONTAINER_PORT, 0); port.put(MarathonConstants.PROTOCOL, MarathonConstants.TCP); port.put(MarathonConstants.HOST_PORT, 0); port.put(MarathonConstants.PORT_NAME, portName); ports.add(port); } return ports; } protected String getExecutorCommand(int containerIndex) { String[] commands = SchedulerUtils.getExecutorCommand(config, runtime, containerIndex, Arrays.asList(MarathonConstants.PORT_LIST)); return "cd $MESOS_SANDBOX && " + Joiner.on(" ").join(commands); } }