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

io.smallrye.stork.loadbalancer.leastresponsetime.CallStatistics Maven / Gradle / Ivy

package io.smallrye.stork.loadbalancer.leastresponsetime;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import io.smallrye.stork.spi.CallStatisticsCollector;

public class CallStatistics implements CallStatisticsCollector {

    private final AtomicLong callCount = new AtomicLong(1);
    private final ConcurrentHashMap storage = new ConcurrentHashMap<>();

    // denoted in comments as e
    static double errorImportanceDeclineFactor = 0.999;
    // e^0, e^1, e^2, e^4, e^8,
    static double[] declineFactorPowers = new double[16];

    static double responseTimeFactor = 0.5;

    // declineFactorPowers[i] = declineFactor^2^i
    // then for n = \sum_{i=0}^{k} 2 ^ f(i) where f(i) \in {0,1},
    // rescaled error rate is \prod_{i=0}^{k}[f(i) == 1]declineFactorPowers[i]

    static {
        declineFactorPowers[0] = 1;
        declineFactorPowers[1] = errorImportanceDeclineFactor;
        for (int i = 2; i < 16; i++) {
            declineFactorPowers[i] = declineFactorPowers[i - 1] * declineFactorPowers[i - 1];
        }
    }

    @Override
    public void storeResult(long id, long timeInNs, Throwable error) {
        long callIdx = callCount.incrementAndGet();

        while (true) {
            CallsData oldData = storage.get(id);
            if (oldData != null) {
                CallsData newData;
                if (error == null) {
                    double newTotalTime = oldData.weightedTotalTime * responseTimeFactor + timeInNs;
                    double newWeightSum = oldData.weightSum * responseTimeFactor + 1;

                    newData = new CallsData(callIdx, oldData.lastFailure, newTotalTime, newWeightSum,
                            oldData.weightedErrorCount);
                } else {
                    double rescaledFailureRate = oldData.scaledErrorCount(callIdx - oldData.lastFailure);
                    newData = new CallsData(oldData.lastSuccess, callIdx, oldData.weightedTotalTime, oldData.weightSum,
                            rescaledFailureRate + 1);
                }
                if (storage.replace(id, oldData, newData)) {
                    break; // otherwise, try once again, until success
                }
            } else {
                // no previously stored data
                CallsData newData;
                if (error == null) {
                    newData = new CallsData(callIdx, 0, timeInNs, 1, 0);
                } else {
                    newData = new CallsData(0, callIdx, 0, 0, 1);
                }

                if (storage.put(callIdx, newData) == null) {
                    break; // success if there was no data (inserted in the meantime)
                }
            }
        }
    }

    // TODO clearing the data for stored service instances if disappeared?
    // TODO or clearing it periodically, etc
    public CallsData statsForInstance(long id) {
        return storage.get(id);
    }

    public long currentCall() {
        return callCount.get();
    }

    public CallsData init(long id) {
        CallsData result = new CallsData(0, 0, 0, 0, 0);
        storage.put(id, result);
        return result;
    }

    public static class CallsData {
        final long lastFailure;
        final long lastSuccess;
        final double weightedTotalTime;
        final double weightSum;
        final double weightedErrorCount;
        final AtomicReference forcedAttemptInProgress = new AtomicReference<>(false);

        private CallsData(long lastSuccess, long lastFailure, double weighedTotalTime, double weightSum,
                double weighedErrorCount) {
            this.lastFailure = lastFailure;
            this.lastSuccess = lastSuccess;
            this.weightedTotalTime = weighedTotalTime;
            this.weightSum = weightSum;
            this.weightedErrorCount = weighedErrorCount;
        }

        /**
         * in time, the failures that happened long time (many requests) ago should have decreasing impact.
         * This function decreases the impact of failures that happened long time ago.
         *
         * @param timeSinceLastError amount of requests made, successfully or not, since the last recorded error.
         *        This contains all requests made with the current load balancer, not only the ones made
         *        through this service instance
         * @return new weighed error count
         */
        // TODO : test
        public double scaledErrorCount(long timeSinceLastError) {
            if (weightedErrorCount == 0) {
                return 0;
            }

            // 1 0 1 0 0 0 0 0 is probably better than 0 1 1 1 1 1 1 1 1 1 or even 0 0 1 1 1 1 1 1 1
            // we need something reasonably decreasing with timeSinceLastError increasing.
            // let's rescale the error by multiplying it by e ^ timeSinceLastError
            double result = weightedErrorCount;
            for (int i = 0; i < 16; i++) {
                int powerOf2 = 1 << i;
                if ((timeSinceLastError & powerOf2) != 0) {
                    result *= declineFactorPowers[i];
                }
            }
            return result;
        }

        public double scaledTime() {
            return weightedTotalTime / weightSum;
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy