com.github.phantomthief.failover.impl.PriorityFailover Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of simple-failover Show documentation
Show all versions of simple-failover Show documentation
A simple failover library for Java
package com.github.phantomthief.failover.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import com.github.phantomthief.failover.SimpleFailover;
import com.github.phantomthief.failover.impl.PriorityFailoverBuilder.PriorityFailoverConfig;
import com.github.phantomthief.failover.impl.PriorityFailoverBuilder.ResConfig;
/**
* @author huangli
* Created on 2020-01-16
*/
@ThreadSafe
public class PriorityFailover implements SimpleFailover, AutoCloseable {
private final PriorityFailoverConfig config;
private final PriorityFailoverCheckTask checkTask;
private final HashMap> resourcesMap;
private final GroupInfo[] groups;
private final boolean concurrentCtrl;
@SuppressWarnings("checkstyle:VisibilityModifier")
static class ResInfo {
final T resource;
final int priority;
final double maxWeight;
final double minWeight;
/**
* if not null, indicate that config.concurrencyControl=true
*/
@Nullable
final Concurrency concurrency;
volatile double currentWeight;
ResInfo(T resource, int priority, double maxWeight, double minWeight,
double initWeight, boolean concurrencyCtrl) {
this.resource = resource;
this.priority = priority;
this.maxWeight = maxWeight;
this.minWeight = minWeight;
this.currentWeight = initWeight;
if (concurrencyCtrl) {
concurrency = new Concurrency();
} else {
concurrency = null;
}
}
}
@SuppressWarnings("checkstyle:VisibilityModifier")
static class GroupInfo {
final int priority;
@Nonnull
final ResInfo[] resources;
final double totalMaxWeight;
final boolean maxWeightSame;
int roundRobinIndex;
volatile GroupWeightInfo groupWeightInfo;
GroupInfo(int priority, @Nonnull ResInfo[] resources, double totalMaxWeight,
boolean maxWeightSame, GroupWeightInfo groupWeightInfo) {
this.priority = priority;
this.resources = resources;
this.totalMaxWeight = totalMaxWeight;
this.maxWeightSame = maxWeightSame;
this.groupWeightInfo = groupWeightInfo;
}
}
@SuppressWarnings("checkstyle:VisibilityModifier")
static class GroupWeightInfo {
final boolean roundRobin;
final double totalCurrentWeight;
//
final double[] currentWeightCopy;
final double healthyRate;
GroupWeightInfo(double totalCurrentWeight, double healthyRate, double[] currentWeightCopy,
boolean roundRobin) {
this.totalCurrentWeight = totalCurrentWeight;
this.healthyRate = healthyRate;
this.currentWeightCopy = currentWeightCopy;
this.roundRobin = roundRobin;
}
}
@SuppressWarnings("checkstyle:VisibilityModifier")
static class Concurrency {
final AtomicInteger atomicValue = new AtomicInteger(0);
Concurrency() {
}
public void incr() {
atomicValue.updateAndGet(v -> Math.max(v + 1, 0));
}
public void decr() {
atomicValue.updateAndGet(v -> Math.max(v - 1, 0));
}
public int get() {
return atomicValue.get();
}
}
public static class ResStatus {
private double maxWeight;
private double minWeight;
private int priority;
private double currentWeight;
private int concurrency;
public double getMaxWeight() {
return maxWeight;
}
public double getMinWeight() {
return minWeight;
}
public int getPriority() {
return priority;
}
public double getCurrentWeight() {
return currentWeight;
}
public int getConcurrency() {
return concurrency;
}
}
@SuppressWarnings("unchecked")
PriorityFailover(PriorityFailoverConfig config) {
Objects.requireNonNull(config.getResources());
this.config = config;
int resCount = config.getResources().size();
this.resourcesMap = new HashMap<>(resCount * 3);
this.concurrentCtrl = config.isConcurrencyControl();
TreeMap>> priorityMap = new TreeMap<>();
for (Entry en : config.getResources().entrySet()) {
T res = en.getKey();
ResConfig rc = en.getValue();
int priority = rc.getPriority();
ResInfo ri = new ResInfo<>(res, priority, rc.getMaxWeight(),
rc.getMinWeight(), rc.getInitWeight(), config.isConcurrencyControl());
this.resourcesMap.put(res, ri);
priorityMap.compute(priority, (k, v) -> {
if (v == null) {
v = new ArrayList<>();
}
v.add(ri);
return v;
});
}
groups = new GroupInfo[priorityMap.size()];
int groupIndex = 0;
for (Entry>> en : priorityMap.entrySet()) {
final int priority = en.getKey();
ArrayList> list = en.getValue();
Collections.shuffle(list);
final ResInfo[] resources = list.toArray(new ResInfo[list.size()]);
double totalMaxWeight = 0;
double totalCurrentWeight = 0;
double firstMaxWeight = resources[0].maxWeight;
boolean maxWeightSame = true;
double[] currentWeightCopy = new double[resources.length];
for (int i = 0; i < resources.length; i++) {
ResInfo ri = resources[i];
totalMaxWeight += ri.maxWeight;
totalCurrentWeight += ri.currentWeight;
currentWeightCopy[i] = ri.currentWeight;
if (ri.maxWeight != firstMaxWeight) {
maxWeightSame = false;
}
}
GroupWeightInfo groupWeightInfo = new GroupWeightInfo(totalCurrentWeight,
totalCurrentWeight / totalMaxWeight, currentWeightCopy,
maxWeightSame && totalCurrentWeight == totalMaxWeight);
groups[groupIndex++] = new GroupInfo<>(priority, resources,
totalMaxWeight, maxWeightSame, groupWeightInfo);
}
checkTask = new PriorityFailoverCheckTask<>(config, this);
}
public static PriorityFailoverBuilder newBuilder() {
return new PriorityFailoverBuilder<>();
}
@Override
public void success(@Nonnull T object) {
processWeight(object, true);
}
@Override
public void fail(@Nonnull T object) {
processWeight(object, false);
checkTask.ensureStart();
}
private void processWeight(@Nonnull T object, boolean success) {
ResInfo resInfo = resourcesMap.get(object);
if (resInfo == null) {
return;
}
if (resInfo.concurrency != null) {
resInfo.concurrency.decr();
}
updateWeight(success, resInfo, config, groups);
}
static void updateWeight(boolean success, ResInfo resInfo, PriorityFailoverConfig config,
GroupInfo[] groups) {
double maxWeight = resInfo.maxWeight;
double minWeight = resInfo.minWeight;
double currentWeight = resInfo.currentWeight;
if ((success && currentWeight >= maxWeight) || (!success && currentWeight <= minWeight)) {
return;
}
int priority = resInfo.priority;
T res = resInfo.resource;
double newWeight;
if (success) {
newWeight = config.getWeightFunction().success(maxWeight,
minWeight, priority, currentWeight, res);
} else {
newWeight = config.getWeightFunction().fail(maxWeight,
minWeight, priority, currentWeight, res);
}
newWeight = Math.min(newWeight, maxWeight);
newWeight = Math.max(newWeight, minWeight);
if (newWeight == currentWeight) {
return;
}
resInfo.currentWeight = newWeight;
updateGroupHealthy(priority, groups);
WeightListener listener = config.getWeightListener();
if (listener != null) {
if (success) {
listener.onSuccess(maxWeight, minWeight, priority, currentWeight, newWeight, res);
} else {
listener.onFail(maxWeight, minWeight, priority, currentWeight, newWeight, res);
}
}
}
static void updateGroupHealthy(int priority, GroupInfo[] groups) {
for (GroupInfo psi : groups) {
if (psi.priority == priority) {
double sumCurrentWeight = 0.0;
ResInfo[] resources = psi.resources;
int resCount = resources.length;
double[] weightCopy = new double[resCount];
for (int i = 0; i < resCount; i++) {
ResInfo ri = resources[i];
sumCurrentWeight += ri.currentWeight;
weightCopy[i] = ri.currentWeight;
}
psi.groupWeightInfo = new GroupWeightInfo(sumCurrentWeight,
sumCurrentWeight / psi.totalMaxWeight, weightCopy,
psi.maxWeightSame && sumCurrentWeight == psi.totalMaxWeight);
}
}
}
@Override
public void down(@Nonnull T object) {
ResInfo resInfo = resourcesMap.get(object);
if (resInfo == null) {
return;
}
if (resInfo.concurrency != null) {
resInfo.concurrency.decr();
}
double oldWeight = resInfo.currentWeight;
resInfo.currentWeight = resInfo.minWeight;
updateGroupHealthy(resInfo.priority, groups);
WeightListener listener = config.getWeightListener();
if (listener != null) {
listener.onFail(resInfo.maxWeight, resInfo.minWeight,
resInfo.priority, oldWeight, resInfo.currentWeight, resInfo.resource);
}
checkTask.ensureStart();
}
@Nullable
@Override
public T getOneAvailable() {
return getOneAvailableExclude(Collections.emptyList());
}
@Nullable
@Override
public T getOneAvailableExclude(@Nonnull Collection exclusions) {
int groupCount = groups.length;
if (groupCount == 0) {
return null;
}
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
int preferGroupIndex;
if (groupCount == 1) {
preferGroupIndex = 0;
} else {
preferGroupIndex = selectGroup(threadLocalRandom);
}
for (int i = 0; i < groupCount; i++) {
ResInfo ri = findOneInRegion(threadLocalRandom, preferGroupIndex, exclusions);
if (ri != null) {
if (ri.concurrency != null) {
ri.concurrency.incr();
}
return ri.resource;
}
preferGroupIndex = (preferGroupIndex + 1) % groupCount;
}
return null;
}
private ResInfo findOneInRegion(ThreadLocalRandom threadLocalRandom, int preferGroupIndex,
@Nonnull Collection exclusions) {
GroupInfo groupInfo = groups[preferGroupIndex];
ResInfo[] resources = groupInfo.resources;
int resCount = resources.length;
GroupWeightInfo groupWeightInfo = groupInfo.groupWeightInfo;
boolean conCtrl = this.concurrentCtrl;
if (exclusions.isEmpty() && !conCtrl) {
if (groupWeightInfo.roundRobin) {
int roundRobinIndex = groupInfo.roundRobinIndex;
groupInfo.roundRobinIndex = (roundRobinIndex + 1) % resCount;
return resources[roundRobinIndex];
} else {
double totalCurrentWeight = groupWeightInfo.totalCurrentWeight;
if (totalCurrentWeight <= 0) {
return null;
}
double random = threadLocalRandom.nextDouble(totalCurrentWeight);
double x = 0;
double[] weightCopy = groupWeightInfo.currentWeightCopy;
for (int i = 0; i < resCount; i++) {
x += weightCopy[i];
if (random < x) {
return resources[i];
}
}
}
} else {
double sumWeight = 0;
double[] weights = new double[resCount];
double[] weightCopy = groupWeightInfo.currentWeightCopy;
for (int i = 0; i < resCount; i++) {
ResInfo ri = resources[i];
if (!exclusions.contains(ri.resource)) {
double w = weightCopy[i];
if (conCtrl) {
int c = ri.concurrency.get();
w = w / (1.0 + c);
}
weights[i] = w;
sumWeight += w;
} else {
weights[i] = -1;
}
}
if (sumWeight <= 0) {
return null;
}
double random = threadLocalRandom.nextDouble(sumWeight);
double x = 0;
for (int i = 0; i < resCount; i++) {
double w = weights[i];
if (w < 0) {
continue;
} else {
x += w;
}
if (random < x) {
return resources[i];
}
}
}
// maybe precise problem, return first one which is not excluded
for (ResInfo ri : resources) {
if (ri.currentWeight > 0 && !exclusions.contains(ri.resource)) {
return ri;
}
}
return null;
}
int selectGroup(ThreadLocalRandom random) {
GroupInfo[] gs = this.groups;
int groupCount = gs.length;
double factor = config.getPriorityFactor();
double groupRandom = -1.0;
for (int i = 0; i < groupCount; i++) {
GroupInfo group = gs[i];
double healthyRate = group.groupWeightInfo.healthyRate;
if (factor == Double.MAX_VALUE && healthyRate > 0) {
return i;
}
double finalHealthyRate = healthyRate * factor;
if (finalHealthyRate >= 1.0) {
return i;
} else {
if (groupRandom < 0) {
groupRandom = random.nextDouble();
}
if (groupRandom < finalHealthyRate) {
return i;
} else {
// regenerate a random value between 0 and 1 for next use
groupRandom = (groupRandom - finalHealthyRate) / (1.0 - finalHealthyRate);
}
}
}
return 0;
}
@Override
public void close() {
checkTask.close();
}
public ResStatus getResourceStatus(T resource) {
ResInfo resInfo = resourcesMap.get(resource);
if (resInfo == null) {
return null;
} else {
ResStatus status = new ResStatus();
status.maxWeight = resInfo.maxWeight;
status.minWeight = resInfo.minWeight;
status.priority = resInfo.priority;
status.currentWeight = resInfo.currentWeight;
if (resInfo.concurrency != null) {
status.concurrency = resInfo.concurrency.get();
}
return status;
}
}
public List getAll() {
return new ArrayList<>(resourcesMap.keySet());
}
HashMap> getResourcesMap() {
return resourcesMap;
}
GroupInfo[] getGroups() {
return groups;
}
PriorityFailoverCheckTask getCheckTask() {
return checkTask;
}
PriorityFailoverConfig getConfig() {
return config;
}
}