// 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.packing.utils;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import com.twitter.heron.api.generated.TopologyAPI;
import com.twitter.heron.common.basics.ByteAmount;
import com.twitter.heron.spi.packing.PackingException;
import com.twitter.heron.spi.packing.Resource;
import com.twitter.heron.spi.utils.TopologyUtils;
/**
* Shared utilities for packing algorithms
*/
public final class PackingUtils {
private static final Logger LOG = Logger.getLogger(PackingUtils.class.getName());
private static final ByteAmount MIN_RAM_PER_INSTANCE = ByteAmount.fromMegabytes(192);
private PackingUtils() {
}
/**
* Verifies the Instance has enough RAM and that it can fit within the container limits.
*
* @param instanceResources The resources allocated to the instance
* @throws PackingException if the instance is invalid
*/
private static void assertIsValidInstance(Resource instanceResources,
ByteAmount minInstanceRam,
Resource maxContainerResources,
int paddingPercentage) throws PackingException {
if (instanceResources.getRam().lessThan(minInstanceRam)) {
throw new PackingException(String.format(
"Instance requires ram %s which is less than the minimum ram per instance of %s",
instanceResources.getRam(), minInstanceRam));
}
ByteAmount instanceRam = instanceResources.getRam().increaseBy(paddingPercentage);
if (instanceRam.greaterThan(maxContainerResources.getRam())) {
throw new PackingException(String.format(
"This instance requires containers of at least %s ram. The current max container "
+ "size is %s",
instanceRam, maxContainerResources.getRam()));
}
double instanceCpu = Math.round(PackingUtils.increaseBy(
instanceResources.getCpu(), paddingPercentage));
if (instanceCpu > maxContainerResources.getCpu()) {
throw new PackingException(String.format(
"This instance requires containers with at least %s cpu cores. The current max container"
+ "size is %s cores",
instanceCpu > maxContainerResources.getCpu(), maxContainerResources.getCpu()));
}
ByteAmount instanceDisk = instanceResources.getDisk().increaseBy(paddingPercentage);
if (instanceDisk.greaterThan(maxContainerResources.getDisk())) {
throw new PackingException(String.format(
"This instance requires containers of at least %s disk. The current max container"
+ "size is %s",
instanceDisk, maxContainerResources.getDisk()));
}
}
public static Resource getResourceRequirement(String component,
Map<String, ByteAmount> componentRamMap,
Resource defaultInstanceResource,
Resource maxContainerResource,
int paddingPercentage) {
ByteAmount instanceRam = defaultInstanceResource.getRam();
if (componentRamMap.containsKey(component)) {
instanceRam = componentRamMap.get(component);
}
assertIsValidInstance(defaultInstanceResource.cloneWithRam(instanceRam),
MIN_RAM_PER_INSTANCE, maxContainerResource, paddingPercentage);
return defaultInstanceResource.cloneWithRam(instanceRam);
}
public static long increaseBy(long value, int paddingPercentage) {
return value + (paddingPercentage * value) / 100;
}
public static double increaseBy(double value, int paddingPercentage) {
return value + (paddingPercentage * value) / 100;
}
/**
* Identifies which components need to be scaled given specific scaling direction
*
* @return Map < component name, scale factor >
*/
public static Map<String, Integer> getComponentsToScale(Map<String,
Integer> componentChanges, ScalingDirection scalingDirection) {
Map<String, Integer> componentsToScale = new HashMap<String, Integer>();
for (String component : componentChanges.keySet()) {
int parallelismChange = componentChanges.get(component);
if (scalingDirection.includes(parallelismChange)) {
componentsToScale.put(component, parallelismChange);
}
}
return componentsToScale;
}
/**
* Identifies the resources reclaimed by the components that will be scaled down
*
* @return Total resources reclaimed
*/
public static Resource computeTotalResourceChange(TopologyAPI.Topology topology,
Map<String, Integer> componentChanges,
Resource defaultInstanceResources,
ScalingDirection scalingDirection) {
double cpu = 0;
ByteAmount ram = ByteAmount.ZERO;
ByteAmount disk = ByteAmount.ZERO;
Map<String, ByteAmount> ramMap = TopologyUtils.getComponentRamMapConfig(topology);
Map<String, Integer> componentsToScale = PackingUtils.getComponentsToScale(
componentChanges, scalingDirection);
for (String component : componentsToScale.keySet()) {
int parallelismChange = Math.abs(componentChanges.get(component));
cpu += parallelismChange * defaultInstanceResources.getCpu();
disk = disk.plus(defaultInstanceResources.getDisk().multiply(parallelismChange));
if (ramMap.containsKey(component)) {
ram = ram.plus(ramMap.get(component).multiply(parallelismChange));
} else {
ram = ram.plus(defaultInstanceResources.getRam().multiply(parallelismChange));
}
}
return new Resource(cpu, ram, disk);
}
public enum ScalingDirection {
UP,
DOWN;
boolean includes(int parallelismChange) {
switch (this) {
case UP:
return parallelismChange > 0;
case DOWN:
return parallelismChange < 0;
default:
throw new IllegalArgumentException(String.format("Not valid parallelism change: %d",
parallelismChange));
}
}
}
}