All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.phantomthief.failover.impl.PriorityFailover Maven / Gradle / Ivy

There is a newer version: 0.1.32
Show newest version
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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy