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

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

There is a newer version: 0.1.32
Show newest version
package com.github.phantomthief.failover.impl;

import static com.github.phantomthief.tuple.Tuple.tuple;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toSet;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.IntUnaryOperator;
import java.util.function.Predicate;

import javax.annotation.Nullable;

import org.slf4j.Logger;

import com.github.phantomthief.failover.Failover;
import com.github.phantomthief.tuple.TwoTuple;
import com.github.phantomthief.util.MoreSuppliers.CloseableSupplier;
import com.google.common.collect.ImmutableList;

/**
 * 默认权重记录
 * fail时权重下降
 * success时权重恢复
 *
 * @author w.vela
 */
public class WeightFailover implements Failover, Closeable {

    private static final Logger logger = getLogger(WeightFailover.class);

    private final IntUnaryOperator failReduceWeight;
    private final IntUnaryOperator successIncreaseWeight;

    private final ConcurrentMap initWeightMap;
    private final ConcurrentMap currentWeightMap;
    @SuppressWarnings("checkstyle:VisibilityModifier")
    final CloseableSupplier> recoveryFuture;
    private final Consumer onMinWeight;
    private final int minWeight;

    /**
     * {@code null} if this feature is off.
     */
    private final Integer weightOnMissingNode;

    /**
     * 用于实现基于上下文的重试过滤逻辑
     */
    @Nullable
    private final Predicate filter;

    @SuppressWarnings("checkstyle:VisibilityModifier")
    AtomicBoolean closed = new AtomicBoolean(false);

    private AtomicInteger allAvailableVersion = new AtomicInteger();

    @SuppressWarnings({"checkstyle:VisibilityModifier"})
    private static class AllAvailable {
        int version;
        List allAvailable;
    }

    private volatile AllAvailable allAvailable;

    WeightFailover(WeightFailoverBuilder builder) {
        this.minWeight = builder.minWeight;
        this.failReduceWeight = builder.failReduceWeight;
        this.successIncreaseWeight = builder.successIncreaseWeight;
        this.initWeightMap = new ConcurrentHashMap<>(builder.initWeightMap);
        this.currentWeightMap = new ConcurrentHashMap<>(builder.initWeightMap);
        this.onMinWeight = builder.onMinWeight;
        this.weightOnMissingNode = builder.weightOnMissingNode;
        this.filter = builder.filter;
        this.allAvailable = new AllAvailable<>();
        this.allAvailable.allAvailable = ImmutableList.copyOf(builder.initWeightMap.keySet());
        this.allAvailable.version = allAvailableVersion.get();
        WeightFailoverCheckTask t = new WeightFailoverCheckTask<>(this, builder, closed,
                initWeightMap, currentWeightMap, allAvailableVersion);
        this.recoveryFuture = t.lazyFuture();
    }

    /**
     * better use {@link #newGenericBuilder()} for type safe
     */
    @Deprecated
    public static WeightFailoverBuilder newBuilder() {
        return new WeightFailoverBuilder<>();
    }

    public static  GenericWeightFailoverBuilder newGenericBuilder() {
        return new GenericWeightFailoverBuilder<>(newBuilder());
    }

    @Override
    public void close() {
        closed.set(true);
        tryCloseRecoveryScheduler(recoveryFuture, this.toString());
    }

    static void tryCloseRecoveryScheduler(CloseableSupplier> recoveryFuture, String name) {
        synchronized (recoveryFuture) {
            recoveryFuture.ifPresent(future -> {
                if (!future.isCancelled()) {
                    if (!future.cancel(true)) {
                        logger.warn("fail to close failover:{}", name);
                    }
                }
            });
        }
    }

    @Override
    public List getAll() {
        return new ArrayList<>(initWeightMap.keySet());
    }

    @Override
    public void fail(T object) {
        if (object == null) {
            logger.warn("invalid fail call, null object found.");
            return;
        }
        currentWeightMap.compute(object, (k, oldValue) -> {
            if (oldValue == null) {
                if (weightOnMissingNode == null) {
                    logger.warn("invalid fail obj:{}, it's not in original list.", object);
                    return null;
                } else {
                    oldValue = weightOnMissingNode;
                    initWeightMap.putIfAbsent(object, weightOnMissingNode);
                }
            }
            int initWeight = initWeightMap.get(k);
            int result = max(minWeight, oldValue - failReduceWeight.applyAsInt(initWeight));
            if (onMinWeight != null) {
                if (result == minWeight && result != oldValue) {
                    onMinWeight.accept(object);
                }
            }
            if (result == 0) {
                logger.warn("found down object:{}", k);
                recoveryFuture.get();
            }
            return result;
        });
        allAvailableVersion.incrementAndGet();
    }

    @Override
    public void down(T object) {
        if (object == null) {
            logger.warn("invalid fail call, null object found.");
            return;
        }
        currentWeightMap.compute(object, (k, oldValue) -> {
            if (oldValue == null) {
                if (weightOnMissingNode == null) {
                    logger.warn("invalid fail obj:{}, it's not in original list.", object);
                    return null;
                } else {
                    oldValue = weightOnMissingNode;
                    initWeightMap.putIfAbsent(object, weightOnMissingNode);
                }
            }
            int result = minWeight;
            if (onMinWeight != null) {
                if (result != oldValue) {
                    onMinWeight.accept(object);
                }
            }
            if (result == 0) {
                logger.warn("found down object:{}", k);
                recoveryFuture.get();
            }
            return result;
        });
        allAvailableVersion.incrementAndGet();
    }

    @Override
    public List getAvailable() {
        // refresh if need
        int version = allAvailableVersion.get();
        boolean refreshed = false;
        if (allAvailable.version != version) {
            refreshed = true;
            AllAvailable tmp = new AllAvailable<>();
            tmp.version = version;
            tmp.allAvailable = doGetAvailable();
            allAvailable = tmp;
        }


        if (filter == null) {
            return allAvailable.allAvailable;
        } else {
            if (refreshed) {
                return allAvailable.allAvailable;
            } else {
                return doGetAvailable();
            }
        }
    }

    private List doGetAvailable() {
        List result = new ArrayList<>(currentWeightMap.size());
        for (Entry entry : currentWeightMap.entrySet()) {
            T item = entry.getKey();
            if (entry.getValue() > 0 && (filter == null || filter.test(item))) {
                result.add(item);
            }
        }
        return unmodifiableList(result);
    }

    @Override
    public T getOneAvailable() {
        // TODO better using a snapshot current Weight or a new stateful Weight
        List available = getAvailable(1);
        return available.isEmpty() ? null : available.get(0);
    }

    @Override
    public List getAvailableExclude(Collection exclusions) {
        return getAvailable(MAX_VALUE, exclusions);
    }

    @Nullable
    @Override
    public T getOneAvailableExclude(Collection exclusions) {
        List result = getAvailable(1, exclusions);
        return result.isEmpty() ? null : result.get(0);
    }

    @Override
    public List getAvailable(int n) {
        return getAvailable(n, emptySet());
    }

    private List getAvailable(int n, Collection exclusions) {
        List> snapshot = new LinkedList<>();
        int sum = 0;
        for (Entry entry : currentWeightMap.entrySet()) {
            int thisWeight = entry.getValue();
            snapshot.add(tuple(entry.getKey(), thisWeight));
            sum += thisWeight;
        }
        List result = new ArrayList<>();
        if (sum > 0) {
            int size = snapshot.size();
            for (int i = 0; i < size; i++) {
                if (sum == 0) {
                    break;
                }
                if (result.size() == n) {
                    break;
                }
                int left = ThreadLocalRandom.current().nextInt(sum);
                Iterator> iterator = snapshot.iterator();
                while (iterator.hasNext()) {
                    TwoTuple candidate = iterator.next();
                    int entryWeight = candidate.getSecond();
                    if (left < entryWeight) {
                        T obj = candidate.getFirst();
                        if (!exclusions.contains(obj) && (filter == null || filter.test(obj))) {
                            result.add(obj);
                        }
                        if (result.size() == n) {
                            break;
                        }
                        iterator.remove();
                        sum -= entryWeight;
                        break;
                    }
                    left -= entryWeight;
                }
            }
        }
        return result;
    }

    @Override
    public void success(T object) {
        boolean[] availableChanged = { false };
        currentWeightMap.compute(object, (k, oldValue) -> {
            if (oldValue == null) {
                if (weightOnMissingNode == null) {
                    logger.warn("invalid fail obj:{}, it's not in original list.", object);
                    return null;
                } else {
                    availableChanged[0] = true;
                    oldValue = weightOnMissingNode;
                    initWeightMap.putIfAbsent(object, weightOnMissingNode);
                }
            }
            int initWeight = initWeightMap.get(k);
            int weight = min(initWeight, oldValue + successIncreaseWeight.applyAsInt(initWeight));
            if (oldValue <= 0 && weight > 0) {
                availableChanged[0] = true;
            }
            return weight;
        });
        if (availableChanged[0]) {
            allAvailableVersion.incrementAndGet();
        }
    }

    @Override
    public Set getFailed() {
        return currentWeightMap.entrySet().stream()
                .filter(entry -> entry.getValue() == 0)
                .map(Entry::getKey)
                .collect(toSet());
    }

    int currentWeight(T obj) {
        return currentWeightMap.get(obj);
    }

    int initWeight(T obj) {
        return initWeightMap.get(obj);
    }

    @Override
    public String toString() {
        return "WeightFailover [" + initWeightMap + "]" + "@" + Integer.toHexString(hashCode());
    }
}