/*
* Copyright 2014-2016 CyberVision, Inc.
*
* 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 org.kaaproject.kaa.server.control.service.loadmgmt.dynamicmgmt;
import org.kaaproject.kaa.server.common.thrift.gen.operations.RedirectionRule;
import org.kaaproject.kaa.server.common.zk.gen.LoadInfo;
import org.kaaproject.kaa.server.control.service.loadmgmt.dynamicmgmt.OperationsServerLoadHistory.OperationsServerLoad;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class EndpointCountRebalancer implements Rebalancer {
private static final Logger LOG = LoggerFactory.getLogger(EndpointCountRebalancer.class);
private static final int DEFAULT_MIN_DIFF = 5000;
private static final double DEFAULT_MIN_INIT_REDIRECT = 0.75;
private static final double DEFAULT_MIN_SESSION_REDIRECT = 0.0;
private final AtomicLong ruleIdSeq = new AtomicLong();
/**
* Minimum difference between amount of endpoints that need to be present in
* order to trigger rebalancing.
*/
@Value("#{properties[loadmgmt_min_diff]}")
private int minDiff = DEFAULT_MIN_DIFF;
/**
* Maximum redirect probability for new sessions.
*/
@Value("#{properties[loadmgmt_max_init_redirect_probability]}")
private double maxInitRedirectProbability = DEFAULT_MIN_INIT_REDIRECT;
/**
* Maximum redirect probability for existing sessions.
*/
@Value("#{properties[loadmgmt_max_session_redirect_probability]}")
private double maxSessionRedirectProbability = DEFAULT_MIN_SESSION_REDIRECT;
/**
* Load mgmt data recalculation period.
*/
@Value("#{properties[recalculation_period]}")
private int recalculationPeriod;
@Override
public Map<Integer, List<RedirectionRule>> recalculate(
Map<Integer, OperationsServerLoadHistory> opsServerLoadHistory) {
Map<Integer, List<RedirectionRule>> result = new HashMap<>();
if (opsServerLoadHistory.isEmpty()) {
LOG.debug("No ops server load history yet");
return result;
}
if (opsServerLoadHistory.size() == 1) {
LOG.debug("No rebalancing in standalone mode");
return result;
}
Set<Entry<Integer, OperationsServerLoadHistory>> entries = opsServerLoadHistory.entrySet();
for (Entry<Integer, OperationsServerLoadHistory> accessPointHistory : entries) {
LOG.debug("Access point: {} has {} history items",
accessPointHistory.getKey(), accessPointHistory.getValue().getHistory().size());
for (OperationsServerLoad load : accessPointHistory.getValue().getHistory()) {
LOG.debug("History: {}", load);
}
}
int minLoadedOpsServer = getMinLoadedOpsServer(opsServerLoadHistory);
int minEndpointCount = getLastEndpointCount(opsServerLoadHistory.get(minLoadedOpsServer));
int maxLoadedOpsServer = getMaxLoadedOpsServer(opsServerLoadHistory);
int maxEndpointCount = getLastEndpointCount(opsServerLoadHistory.get(maxLoadedOpsServer));
LOG.info("Min loaded ops server is {} with {} endpoints", minLoadedOpsServer, minEndpointCount);
LOG.info("Max loaded ops server is {} with {} endpoints", maxLoadedOpsServer, maxEndpointCount);
int maxDiff = maxEndpointCount - minEndpointCount;
LOG.info("Max difference between endpoint counts is {}", maxDiff);
if (maxDiff < minDiff) {
LOG.debug("Max endpoint count difference is too small to trigger recalculation. "
+ "Min required diff is {}", minDiff);
return result;
}
int totalLoad = 0;
for (Entry<Integer, OperationsServerLoadHistory> opsEntry : opsServerLoadHistory.entrySet()) {
totalLoad += getLastEndpointCount(opsEntry.getValue());
}
int targetLoad = totalLoad / opsServerLoadHistory.size();
LOG.debug("Target load is {}", targetLoad);
Map<Integer, Double> weights = calculateWeights(opsServerLoadHistory, targetLoad);
for (Entry<Integer, OperationsServerLoadHistory> opsEntry : opsServerLoadHistory.entrySet()) {
double curWeight = weights.get(opsEntry.getKey());
if (curWeight >= 0) {
LOG.debug("No redirection rules for {}", targetLoad);
continue;
}
List<RedirectionRule> redirectionRules = new ArrayList<>();
for (Entry<Integer, Double> targetWeight : weights.entrySet()) {
if (targetWeight.getValue() < 0) {
continue;
}
double initRedirectProbability = Math.abs(curWeight)
* targetWeight.getValue() * maxInitRedirectProbability;
double sessionRedirectProbability = targetWeight.getValue() * maxSessionRedirectProbability;
if (initRedirectProbability > 0 || sessionRedirectProbability > 0) {
RedirectionRule rule = new RedirectionRule(targetWeight.getKey(),
ruleIdSeq.getAndIncrement(), initRedirectProbability,
sessionRedirectProbability, recalculationPeriod * 1000L);
LOG.debug("Calculated new rule for accessPointId: {} -> {}", opsEntry.getKey(), rule);
redirectionRules.add(rule);
}
}
result.put(opsEntry.getKey(), redirectionRules);
}
return result;
}
private Map<Integer, Double> calculateWeights(
Map<Integer, OperationsServerLoadHistory> opsServerLoadHistory, int targetLoad) {
Map<Integer, Double> weights = new LinkedHashMap<>();
double totalPosWeight = 0;
double totalNegWeight = 0;
for (Entry<Integer, OperationsServerLoadHistory> opsEntry : opsServerLoadHistory.entrySet()) {
int curOpsEndpointCount = getLastEndpointCount(opsEntry.getValue());
double weight = targetLoad - curOpsEndpointCount;
if (weight > 0) {
totalPosWeight += weight;
} else {
totalNegWeight = Math.max(totalNegWeight, Math.abs(weight));
}
weights.put(opsEntry.getKey(), weight);
}
for (Entry<Integer, Double> weightEntry : weights.entrySet()) {
double weight = weightEntry.getValue();
if (weight > 0) {
weightEntry.setValue(weightEntry.getValue() / totalPosWeight);
} else {
weightEntry.setValue(weightEntry.getValue() / totalNegWeight);
}
LOG.debug("Calculated redirection weight of {} is {}",
weightEntry.getKey(), weightEntry.getValue());
}
return weights;
}
private int getMinLoadedOpsServer(
Map<Integer, OperationsServerLoadHistory> opsServerLoadHistory) {
int result = 0;
int minCount = Integer.MAX_VALUE;
for (Entry<Integer, OperationsServerLoadHistory> entry : opsServerLoadHistory.entrySet()) {
int endpointCount = getLastEndpointCount(entry.getValue());
if (endpointCount < minCount) {
result = entry.getKey();
minCount = endpointCount;
}
}
return result;
}
private int getMaxLoadedOpsServer(
Map<Integer, OperationsServerLoadHistory> opsServerLoadHistory) {
int result = 0;
int maxCount = Integer.MIN_VALUE;
for (Entry<Integer, OperationsServerLoadHistory> entry : opsServerLoadHistory.entrySet()) {
int endpointCount = getLastEndpointCount(entry.getValue());
if (endpointCount > maxCount) {
result = entry.getKey();
maxCount = endpointCount;
}
}
return result;
}
private int getLastEndpointCount(OperationsServerLoadHistory loadHistory) {
List<OperationsServerLoad> history = loadHistory.getHistory();
if (!history.isEmpty()) {
LoadInfo loadInfo = history.get(history.size() - 1).getLoadInfo();
if (loadInfo != null) {
return loadInfo.getEndpointCount();
}
}
return 0;
}
}