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

pl.allegro.tech.hermes.consumers.consumer.rate.maxrate.MaxRateBalancer Maven / Gradle / Ivy

There is a newer version: 2.8.0
Show newest version
package pl.allegro.tech.hermes.consumers.consumer.rate.maxrate;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

class MaxRateBalancer {

    private static final double ALLOWED_DISTRIBUTION_ERROR = 1.0d;

    private final double busyTolerance;
    private final double minMax;
    private final double minAllowedChangePercent;

    MaxRateBalancer(double busyTolerance, double minMax, double minAllowedChangePercent) {
        this.busyTolerance = busyTolerance;
        this.minMax = minMax;
        this.minAllowedChangePercent = minAllowedChangePercent;
    }

    Optional> balance(double subscriptionMax, Set rateInfos) {
        double defaultRate = Math.max(minMax, subscriptionMax / Math.max(1, rateInfos.size()));

        if (shouldResortToDefaults(subscriptionMax, rateInfos)) {
            return Optional.of(balanceDefault(defaultRate, rateInfos));
        }

        List activeConsumerInfos =
                rateInfos.stream().map(ActiveConsumerInfo::convert).collect(Collectors.toList());

        if (subscriptionRateChanged(activeConsumerInfos, subscriptionMax)) {
            return Optional.of(balanceDefault(defaultRate, rateInfos));
        }

        Map> busyOrNot = busyOrNot(activeConsumerInfos);
        List busy = busyOrNot.get(true);
        List notBusy = busyOrNot.get(false)
                .stream()
                .filter(consumer -> !consumer.getRateHistory().getRates().isEmpty())
                .collect(Collectors.toList());

        if (busy.isEmpty()) {
            return Optional.empty();
        }

        double minChange = (minAllowedChangePercent / 100) * subscriptionMax;
        NotBusyBalancer.Result notBusyChanges = handleNotBusy(notBusy, minChange);
        Map busyUpdates =
                handleBusy(minChange, busy, notBusyChanges.getReleasedRate()).calculateNewMaxRates();
        Map notBusyUpdates = notBusyChanges.calculateNewMaxRates();

        return Optional.of(Stream.of(busyUpdates, notBusyUpdates)
                .map(Map::entrySet)
                .flatMap(Collection::stream)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
    }

    private boolean shouldResortToDefaults(double subscriptionMax, Set rateInfos) {
        return anyNewConsumers(rateInfos) || insufficientSubscriptionRate(subscriptionMax, rateInfos.size());
    }

    private Map balanceDefault(double defaultRate, Set rateInfos) {
        return rateInfos.stream()
                .collect(Collectors.toMap(ConsumerRateInfo::getConsumerId,
                        rateInfo -> new MaxRate(defaultRate)));
    }

    private boolean anyNewConsumers(Set rateInfos) {
        return rateInfos.stream().anyMatch(this::isUnassigned);
    }

    private boolean insufficientSubscriptionRate(double subscriptionMax, int consumersCount) {
        return subscriptionMax / consumersCount <= 1.0d;
    }

    private boolean subscriptionRateChanged(List activeConsumerInfos, double subscriptionMax) {
        double sum = activeConsumerInfos.stream().mapToDouble(ActiveConsumerInfo::getMax).sum();
        return Math.abs(sum - subscriptionMax) > ALLOWED_DISTRIBUTION_ERROR;
    }

    private boolean isUnassigned(ConsumerRateInfo rateInfo) {
        return !rateInfo.getMaxRate().isPresent();
    }

    private Map> busyOrNot(List infos) {
        return infos.stream().collect(Collectors.partitioningBy(this::isBusy));
    }

    private boolean isBusy(ActiveConsumerInfo info) {
        return info.getRateHistory().getRates().stream()
                .mapToDouble(Double::doubleValue).average().orElse(0) > 1.0 - busyTolerance;
    }

    private NotBusyBalancer.Result handleNotBusy(List notBusy, double minChange) {
        return new NotBusyBalancer(notBusy, minChange, minMax).balance();
    }

    private BusyBalancer.Result handleBusy(double minChange, List busy, double freedByNotBusy) {
        return new BusyBalancer(busy, freedByNotBusy, minChange).balance();
    }

    private static class ActiveConsumerInfo {

        private final String consumerId;
        private final RateHistory rateHistory;
        private final Double max;

        ActiveConsumerInfo(String consumerId, RateHistory rateHistory, Double max) {
            this.consumerId = consumerId;
            this.rateHistory = rateHistory;
            this.max = max;
        }

        static ActiveConsumerInfo convert(ConsumerRateInfo rateInfo) {
            return new ActiveConsumerInfo(
                    rateInfo.getConsumerId(),
                    rateInfo.getHistory(),
                    rateInfo.getMaxRate().get().getMaxRate());
        }

        String getConsumerId() {
            return consumerId;
        }

        RateHistory getRateHistory() {
            return rateHistory;
        }

        Double getMax() {
            return max;
        }
    }

    private static class NotBusyBalancer {

        private final List consumerInfos;
        private final double minChange;
        private final double minMax;

        NotBusyBalancer(List consumerInfos, double minChange, double minMax) {
            this.consumerInfos = consumerInfos;
            this.minChange = minChange;
            this.minMax = minMax;
        }

        Result balance() {
            List changes = consumerInfos.stream()
                    .map(ri -> {
                        double currentMax = ri.getMax();
                        double toDistribute = takeAwayFromNotBusy(ri.getRateHistory(), currentMax);
                        return new ConsumerRateChange(ri.getConsumerId(), currentMax, -toDistribute);
            }).collect(Collectors.toList());

            return new Result(changes);
        }

        private double takeAwayFromNotBusy(RateHistory history, double currentMax) {
            double usedRatio = history.getRates().get(0); // rate history must be present
            double scalingFactor = 2 / (usedRatio + 1.0) - 1;

            double proposedChange = scalingFactor * currentMax;
            double actualChange = proposedChange > minChange ? proposedChange : 0.0d;

            return currentMax - actualChange > minMax ? actualChange : currentMax - minMax;
        }

        private static class Result {

            private final List changes;
            private final double releasedRate;

            Result(List changes) {
                this.changes = changes;
                this.releasedRate = -changes.stream().mapToDouble(ConsumerRateChange::getRateChange).sum();
            }

            double getReleasedRate() {
                return releasedRate;
            }

            Map calculateNewMaxRates() {
                return changes.stream()
                        .collect(Collectors.toMap(
                                ConsumerRateChange::getConsumerId,
                                change -> new MaxRate(change.getCurrentMax() + change.getRateChange())));
            }
        }
    }

    private static class BusyBalancer {

        private final List consumerInfos;
        private final double freedByNotBusy;
        private final double minChange;

        BusyBalancer(List consumerInfos, double freedByNotBusy, double minChange) {
            this.consumerInfos = consumerInfos;
            this.freedByNotBusy = freedByNotBusy;
            this.minChange = minChange;
        }

        Result balance() {
            double busyMaxSum = consumerInfos.stream().mapToDouble(ActiveConsumerInfo::getMax).sum();

            List shares = consumerInfos.stream()
                    .map(info ->
                            new ConsumerMaxShare(info.getConsumerId(), info.getMax(), info.getMax() / busyMaxSum))
                    .collect(Collectors.toList());

            if (shares.size() == 1) {
                return new Result(distribute(freedByNotBusy, shares));
            }

            double equalShare = busyMaxSum / consumerInfos.size();

            Map> greedyOrNot =
                    shares.stream().collect(
                            Collectors.partitioningBy(share -> share.getCurrentMax() > equalShare + minChange));

            List greedy = greedyOrNot.get(true);
            List notGreedy = greedyOrNot.get(false);

            List greedySubtracts = greedy.stream()
                    .map(share -> {
                        double toDistribute =
                                takeAwayFromGreedy(share.getCurrentMax(), share.getShare(), equalShare);
                        return new ConsumerRateChange(share.getConsumerId(), share.currentMax, -toDistribute);
                    })
                    .collect(Collectors.toList());

            double toDistribute = freedByNotBusy - greedySubtracts.stream()
                    .mapToDouble(ConsumerRateChange::getRateChange)
                    .sum();

            List notGreedyShares = recalculateShare(notGreedy);
            List notGreedyAdds = distribute(toDistribute, notGreedyShares);

            return new Result(
                    Stream.concat(notGreedyAdds.stream(), greedySubtracts.stream())
                            .collect(Collectors.toList())
            );
        }

        private double takeAwayFromGreedy(double currentMax, double share, double equalShare) {
            double scale = 2 / (share + 1.0) - 1;
            double changeProposal = (currentMax / 2) * scale;
            double actualChange = Math.max(changeProposal, minChange);
            return (currentMax - actualChange) > equalShare ? actualChange : currentMax - equalShare;
        }

        private List recalculateShare(List shares) {
            double sum = shares.stream().mapToDouble(ConsumerMaxShare::getCurrentMax).sum();
            return shares.stream()
                    .map(previous -> new ConsumerMaxShare(
                            previous.getConsumerId(),
                            previous.getCurrentMax(),
                            previous.currentMax / sum))
                    .collect(Collectors.toList());
        }

        private List distribute(double maxAmount, List shares) {
            return shares.stream()
                    .map(share -> new ConsumerRateChange(
                            share.consumerId,
                            share.getCurrentMax(),
                            share.getShare() * maxAmount))
                    .collect(Collectors.toList());
        }

        private static class Result {

            private final List changes;

            Result(List changes) {
                this.changes = changes;
            }

            Map calculateNewMaxRates() {
                return changes.stream()
                        .collect(Collectors.toMap(
                                ConsumerRateChange::getConsumerId,
                                change -> new MaxRate(change.getCurrentMax() + change.getRateChange())));
            }
        }

        private static class ConsumerMaxShare {

            private final String consumerId;
            private final double currentMax;
            private final double share;

            ConsumerMaxShare(String consumerId, double currentMax, double share) {
                this.consumerId = consumerId;
                this.currentMax = currentMax;
                this.share = share;
            }

            String getConsumerId() {
                return consumerId;
            }

            double getCurrentMax() {
                return currentMax;
            }

            double getShare() {
                return share;
            }
        }
    }

    private static class ConsumerRateChange {

        private String consumerId;
        private double currentMax;
        private double rateChange;

        ConsumerRateChange(String consumerId, double currentMax, double rateChange) {
            this.consumerId = consumerId;
            this.currentMax = currentMax;
            this.rateChange = rateChange;
        }

        String getConsumerId() {
            return consumerId;
        }

        double getCurrentMax() {
            return currentMax;
        }

        double getRateChange() {
            return rateChange;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy