/* * Copyright (c) 2010-2012 Grid Dynamics Consulting Services, Inc, All Rights Reserved * http://www.griddynamics.com * * This library is free software; you can redistribute it and/or modify it under the terms of * the Apache License; either * version 2.0 of the License, or any later version. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.griddynamics.jagger.engine.e1.scenario; import com.google.common.collect.Maps; import com.google.common.collect.Table; import com.griddynamics.jagger.util.Pair; import com.griddynamics.jagger.util.TimeUtils; import org.apache.commons.math.stat.regression.SimpleRegression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.SortedMap; import static com.griddynamics.jagger.util.DecimalUtil.areEqual; public class DefaultWorkloadSuggestionMaker implements WorkloadSuggestionMaker { private static final Logger log = LoggerFactory.getLogger(DefaultWorkloadSuggestionMaker.class); private static final WorkloadConfiguration FIRST_POINT_CONFIGURATION = WorkloadConfiguration.with(1, 0); private static final int MIN_DELAY = 10; // critical. if delay is too long we are not able to stop threads fast during balancing => // change workload configuration fails by timeout private static final int MAX_DELAY = 1000; private final int maxDiff; public DefaultWorkloadSuggestionMaker(int maxDiff) { this.maxDiff = maxDiff; } @Override public WorkloadConfiguration suggest(BigDecimal desiredTps, NodeTpsStatistics statistics, int maxThreads) { log.debug("Going to suggest workload configuration. desired tps {}. statistics {}", desiredTps, statistics); Table<Integer, Integer, Pair<Long, BigDecimal>> threadDelayStats = statistics.getThreadDelayStats(); if(areEqual(desiredTps, BigDecimal.ZERO)) { return WorkloadConfiguration.with(0, 0); } if (threadDelayStats.isEmpty()) { throw new IllegalArgumentException("Cannot suggest workload configuration"); } if (!threadDelayStats.contains(FIRST_POINT_CONFIGURATION.getThreads(), FIRST_POINT_CONFIGURATION.getDelay())) { log.debug("Statistics is empty. Injecting empty entry"); return FIRST_POINT_CONFIGURATION; } if (threadDelayStats.size() == 2 && areEqual(threadDelayStats.get(1, 0).getSecond(), BigDecimal.ZERO)) { log.warn("Statistics is still empty. Injecting empty entry"); return FIRST_POINT_CONFIGURATION; } Map<Integer, Pair<Long, BigDecimal>> noDelays = threadDelayStats.column(0); log.debug("Calculate next thread count"); Integer threadCount = findClosestPoint(desiredTps, noDelays); if (threadCount == 0) { threadCount = 1; } if (threadCount > maxThreads) { log.warn("{} calculated max {} allowed", threadCount, maxThreads); threadCount = maxThreads; } int currentThreads = statistics.getCurrentWorkloadConfiguration().getThreads(); int diff = threadCount - currentThreads; if (diff > maxDiff) { log.debug("Increasing to {} is required current thread count is {} max allowed diff is {}", new Object[]{threadCount, currentThreads, maxDiff}); return WorkloadConfiguration.with(currentThreads + maxDiff, 0); } diff = currentThreads - threadCount; if (diff > maxDiff) { log.debug("Decreasing to {} is required current thread count is {} max allowed diff is {}", new Object[]{threadCount, currentThreads, maxDiff}); if ((currentThreads - maxDiff) > 1) { return WorkloadConfiguration.with(currentThreads - maxDiff, 0); } else { return WorkloadConfiguration.with(1, 0); } } if (!threadDelayStats.contains(threadCount, 0)) { return WorkloadConfiguration.with(threadCount, 0); } // <delay, <timestamp,tps>> Map<Integer, Pair<Long, BigDecimal>> delays = threadDelayStats.row(threadCount); // not enough statistics to calculate if (delays.size() == 1) { int delay = 0; BigDecimal tpsFromStat = delays.get(0).getSecond(); // try to guess // tpsFromStat can be zero if no statistics was captured till this time if ((tpsFromStat.compareTo(BigDecimal.ZERO) > 0) && (desiredTps.compareTo(BigDecimal.ZERO) > 0)) { BigDecimal oneSecond = new BigDecimal(TimeUtils.secondsToMillis(1)); BigDecimal result = oneSecond.multiply(new BigDecimal(threadCount)).divide(desiredTps, 3, BigDecimal.ROUND_HALF_UP); result = result.subtract(oneSecond.multiply(new BigDecimal(threadCount)).divide(tpsFromStat, 3, BigDecimal.ROUND_HALF_UP)); delay = result.intValue(); } // to have some non zero point in statistics if (delay == 0) { delay = MIN_DELAY; } delay = checkDelayInRange(delay); return WorkloadConfiguration.with(threadCount, delay); } log.debug("Calculate next delay"); Integer delay = findClosestPoint(desiredTps, threadDelayStats.row(threadCount)); delay = checkDelayInRange(delay); return WorkloadConfiguration.with(threadCount, delay); } private static Integer findClosestPoint(BigDecimal desiredTps, Map<Integer, Pair<Long, BigDecimal>> stats) { final int MAX_POINTS_FOR_REGRESSION = 10; SortedMap<Long, Integer> map = Maps.newTreeMap(new Comparator<Long>() { @Override public int compare(Long first, Long second) { return second.compareTo(first); } }); for (Map.Entry<Integer, Pair<Long, BigDecimal>> entry : stats.entrySet()) { map.put(entry.getValue().getFirst(), entry.getKey()); } if (map.size() < 2) { throw new IllegalArgumentException("Not enough stats to calculate point"); } // <time><number of threads> - sorted by time Iterator<Map.Entry<Long, Integer>> iterator = map.entrySet().iterator(); SimpleRegression regression = new SimpleRegression(); Integer tempIndex; double previousValue = -1.0; double value; double measuredTps; log.debug("Selecting next point for balancing"); int indx = 0; while (iterator.hasNext()) { tempIndex = iterator.next().getValue(); if (previousValue < 0.0) { previousValue = tempIndex.floatValue(); } value = tempIndex.floatValue(); measuredTps = stats.get(tempIndex).getSecond().floatValue(); regression.addData(value, measuredTps); log.debug(String.format(" %7.2f %7.2f",value,measuredTps)); indx++; if (indx > MAX_POINTS_FOR_REGRESSION) { break; } } double intercept = regression.getIntercept(); double slope = regression.getSlope(); double approxPoint; // if no slope => use previous number of threads if (Math.abs(slope) > 1e-12) { approxPoint = (desiredTps.doubleValue() - intercept) / slope; } else { approxPoint = previousValue; } // if approximation point is negative - ignore it if (approxPoint < 0) { approxPoint = previousValue; } log.debug(String.format("Next point %7d (target tps: %7.2f)",(int)Math.round(approxPoint),desiredTps.doubleValue())); return (int)Math.round(approxPoint); } private static int checkDelayInRange(int delay) { if (delay < 0) { delay = 0; } if (delay > MAX_DELAY) { delay = MAX_DELAY; } return delay; } }