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

io.ceresdb.rpc.limit.VegasLimit Maven / Gradle / Ivy

There is a newer version: 1.0.5
Show newest version
/*
 * Copyright 2023 CeresDB Project Authors. Licensed under Apache-2.0.
 */
package io.ceresdb.rpc.limit;

import io.ceresdb.common.util.Requires;
import com.netflix.concurrency.limits.MetricIds;
import com.netflix.concurrency.limits.MetricRegistry;
import com.netflix.concurrency.limits.MetricRegistry.SampleListener;
import com.netflix.concurrency.limits.internal.EmptyMetricRegistry;
import com.netflix.concurrency.limits.limit.AbstractLimit;
import com.netflix.concurrency.limits.limit.functions.Log10RootFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * Limiter based on TCP Vegas where the limit increases by alpha if the queue_use is small ({@literal <} alpha)
 * and decreases by alpha if the queue_use is large ({@literal >} beta).
 *
 * Queue size is calculated using the formula, 
 *  queue_use = limit − BWE×RTTnoLoad = limit × (1 − RTTnoLoad/RTTactual)
 *
 * For traditional TCP Vegas alpha is typically 2-3 and beta is typically 4-6.  To allow for better growth and 
 * stability at higher limits we set alpha=Max(3, 10% of the current limit) and beta=Max(6, 20% of the current limit)
 *
 * Refer to {@link com.netflix.concurrency.limits.limit.VegasLimit}
 */
public class VegasLimit extends AbstractLimit {

    private static final Logger LOG = LoggerFactory.getLogger(VegasLimit.class);

    private static final Function LOG10 = Log10RootFunction.create(0);

    public static class Builder {
        private int            initialLimit   = 20;
        private int            maxConcurrency = 1000;
        private MetricRegistry registry       = EmptyMetricRegistry.INSTANCE;
        private double         smoothing      = 1.0;

        private Function alphaFunc        = (limit) -> 3 * LOG10.apply(limit);
        private Function betaFunc         = (limit) -> 6 * LOG10.apply(limit);
        private Function thresholdFunc    = LOG10;
        private Function   increaseFunc     = (limit) -> limit + LOG10.apply(limit.intValue());
        private Function   decreaseFunc     = (limit) -> limit - LOG10.apply(limit.intValue());
        private int                        probeMultiplier  = 30;
        private boolean                    logOnLimitChange = true;

        private Builder() {
        }

        /**
         * The limiter will probe for a new noload RTT every probeMultiplier * current limit
         * iterations.  Default value is 30.
         *
         * @param probeMultiplier probe multiplier, default is 30
         * @return Chainable builder
         */
        public Builder probeMultiplier(final int probeMultiplier) {
            this.probeMultiplier = probeMultiplier;
            return this;
        }

        public Builder alpha(final int alpha) {
            this.alphaFunc = (ignore) -> alpha;
            return this;
        }

        public Builder threshold(final Function threshold) {
            this.thresholdFunc = threshold;
            return this;
        }

        public Builder alpha(final Function alpha) {
            this.alphaFunc = alpha;
            return this;
        }

        public Builder beta(final int beta) {
            this.betaFunc = (ignore) -> beta;
            return this;
        }

        public Builder beta(final Function beta) {
            this.betaFunc = beta;
            return this;
        }

        public Builder increase(final Function increase) {
            this.increaseFunc = increase;
            return this;
        }

        public Builder decrease(final Function decrease) {
            this.decreaseFunc = decrease;
            return this;
        }

        public Builder smoothing(final double smoothing) {
            this.smoothing = smoothing;
            return this;
        }

        public Builder initialLimit(final int initialLimit) {
            this.initialLimit = initialLimit;
            return this;
        }

        public Builder maxConcurrency(final int maxConcurrency) {
            this.maxConcurrency = maxConcurrency;
            return this;
        }

        public Builder logOnLimitChange(final boolean logOnLimitChange) {
            this.logOnLimitChange = logOnLimitChange;
            return this;
        }

        public Builder metricRegistry(final MetricRegistry registry) {
            this.registry = registry;
            return this;
        }

        public VegasLimit build() {
            return new VegasLimit(this);
        }
    }

    public static Builder newBuilder() {
        return new Builder();
    }

    public static VegasLimit newDefault() {
        return newBuilder().build();
    }

    /**
     * Estimated concurrency limit based on our algorithm
     */
    private volatile double estimatedLimit;

    private volatile long rtt_noload = 0;

    /**
     * Maximum allowed limit providing an upper bound failsafe
     */
    private final int maxLimit;

    private final double                     smoothing;
    private final Function alphaFunc;
    private final Function betaFunc;
    private final Function thresholdFunc;
    private final Function   increaseFunc;
    private final Function   decreaseFunc;
    private final SampleListener             rttSampleListener;
    private final int                        probeMultiplier;
    private final boolean                    logOnLimitChange;
    private int                              probeCount = 0;
    private double                           probeJitter;

    private VegasLimit(Builder builder) {
        super(builder.initialLimit);
        this.estimatedLimit = builder.initialLimit;
        this.maxLimit = builder.maxConcurrency;
        this.alphaFunc = builder.alphaFunc;
        this.betaFunc = builder.betaFunc;
        this.increaseFunc = builder.increaseFunc;
        this.decreaseFunc = builder.decreaseFunc;
        this.thresholdFunc = builder.thresholdFunc;
        this.smoothing = builder.smoothing;
        this.probeMultiplier = builder.probeMultiplier;
        this.logOnLimitChange = builder.logOnLimitChange;

        resetProbeJitter();

        this.rttSampleListener = builder.registry.distribution(MetricIds.MIN_RTT_NAME);
    }

    private void resetProbeJitter() {
        this.probeJitter = ThreadLocalRandom.current().nextDouble(0.5, 1);
    }

    private boolean shouldProbe() {
        return this.probeJitter * this.probeMultiplier * this.estimatedLimit <= this.probeCount;
    }

    @Override
    protected int _update(final long startTime, final long rtt, final int inflight, final boolean didDrop) {
        Requires.requireTrue(rtt > 0, "rtt must be > 0 but got " + rtt);

        this.probeCount++;
        if (shouldProbe()) {
            LOG.debug("Probe MinRTT {}.", TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0);
            resetProbeJitter();
            this.probeCount = 0;
            this.rtt_noload = rtt;
            return (int) this.estimatedLimit;
        }

        if (this.rtt_noload == 0 || rtt < this.rtt_noload) {
            LOG.debug("New MinRTT {}.", TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0);
            this.rtt_noload = rtt;
            return (int) this.estimatedLimit;
        }

        this.rttSampleListener.addSample(getRttMillis(this.rtt_noload));

        return updateEstimatedLimit(rtt, inflight, didDrop);
    }

    private int updateEstimatedLimit(final long rtt, final int inflight, final boolean didDrop) {
        final double currLimit = this.estimatedLimit;

        final int queueSize = (int) Math.ceil(currLimit * (1 - (double) this.rtt_noload / rtt));

        double newLimit;
        // Treat any drop (i.e timeout) as needing to reduce the limit
        if (didDrop) {
            newLimit = this.decreaseFunc.apply(currLimit);
            // Prevent upward drift if not close to the limit
        } else if (inflight * 2 < currLimit) {
            return (int) currLimit;
        } else {
            final int alpha = this.alphaFunc.apply((int) currLimit);
            final int beta = this.betaFunc.apply((int) currLimit);
            final int threshold = this.thresholdFunc.apply((int) currLimit);

            // Aggressive increase when no queuing
            if (queueSize <= threshold) {
                newLimit = currLimit + beta;
                // Increase the limit if queue is still manageable
            } else if (queueSize < alpha) {
                newLimit = this.increaseFunc.apply(currLimit);
                // Detecting latency so decrease
            } else if (queueSize > beta) {
                newLimit = this.decreaseFunc.apply(currLimit);
                // We're within he sweet spot so nothing to do
            } else {
                return (int) currLimit;
            }
        }

        newLimit = Math.max(1, Math.min(this.maxLimit, newLimit));
        newLimit = (1 - this.smoothing) * currLimit + this.smoothing * newLimit;

        if (this.logOnLimitChange && (int) newLimit != (int) currLimit) {
            LOG.info("New limit={}, previous limit={}, minRtt={} ms, winRtt={} ms, queueSize={}.", (int) newLimit,
                    (int) currLimit, TimeUnit.NANOSECONDS.toMicros(this.rtt_noload) / 1000.0,
                    TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0, queueSize);
        }

        this.estimatedLimit = newLimit;

        return (int) this.estimatedLimit;
    }

    private long getRttMillis(final long nanos) {
        return TimeUnit.NANOSECONDS.toMillis(nanos);
    }

    @Override
    public String toString() {
        return "VegasLimit [limit=" + getLimit() + //
               ", rtt_noload=" + TimeUnit.NANOSECONDS.toMicros(this.rtt_noload) / 1000.0 + //
               " ms]";
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy