| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.cassandra.service; |
| |
| import java.util.concurrent.ThreadLocalRandom; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.DoubleSupplier; |
| import java.util.function.LongBinaryOperator; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.annotation.Nonnull; |
| import javax.annotation.Nullable; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import accord.utils.Invariants; |
| import accord.utils.RandomSource; |
| |
| import org.apache.cassandra.service.TimeoutStrategy.LatencySourceFactory; |
| import org.apache.cassandra.service.TimeoutStrategy.Wait; |
| |
| import static java.util.concurrent.TimeUnit.MICROSECONDS; |
| import static java.util.concurrent.TimeUnit.NANOSECONDS; |
| import static org.apache.cassandra.service.TimeoutStrategy.parseInMicros; |
| import static org.apache.cassandra.utils.Clock.Global.nanoTime; |
| |
| /** |
| * <p>A strategy for making retry timing decisions for operations. |
| * The strategy is defined by six factors: <ul> |
| * <li> {@link #minMinMicros} |
| * <li> {@link #maxMaxMicros} |
| * <li> {@link #min} |
| * <li> {@link #max} |
| * <li> {@link #waitRandomizer} |
| * <li> {@link #maxAttempts} |
| * </ul> |
| * |
| * <p>The first two represent the absolute upper and lower bound times we are permitted to produce as constants</p> |
| * <p>The next two represent time periods, and may be defined dynamically based on a simple calculation over: <ul> |
| * <li> {@code pX()} recent experienced latency distribution for successful operations, |
| * e.g. {@code p50(rw)} the maximum of read and write median latencies, |
| * {@code p999(r)} the 99.9th percentile of read latencies |
| * <li> {@code attempts} the number of failed attempts made by the operation so far |
| * <li> {@code constant} a user provided floating point constant |
| * </ul> |
| * |
| * <p>Their calculation may take any of these forms |
| * <li> constant {@code $constant$[mu]s} |
| * <li> dynamic constant {@code pX() * constant} |
| * <li> dynamic linear {@code pX() * constant * attempts} |
| * <li> dynamic exponential {@code pX() * constant ^ attempts} |
| * <li> Note: for dynamic exponential, attempts is subtracted by 1, such that times begin at {@code pX() * constant}. |
| * e.g. |
| * <li> {@code 10ms <= p50(rw)*0.66...p99(rw)} |
| * <li> {@code 10ms <= p95(rw)*1.8^attempts <= 100ms} |
| * <li> {@code 5ms <= p50(rw)*0.5} |
| * |
| * <p>These calculations are put together to construct a range from which we draw a random number. |
| * The period we wait for {@code X} will be drawn so that {@code minMin <= min <= max <= maxMax}. |
| * |
| * <p>With the constraint that {@code max} must be {@code spread} greater than {@code min}, |
| * but no greater than its expression-defined maximum. {@code max} will be increased up until |
| * this point, after which {@code min} will be decreased until this gap is imposed. |
| * |
| * <p>The {@link #waitRandomizer} property specifies the manner in which a random value is drawn from the range. |
| * It is defined using one of the following specifiers: |
| * <li> uniform |
| * <li> exp($power$) or exponential($power$) |
| * <li> qexp($power$) or qexponential($power$) or quantizedexponential($power$) |
| * |
| * The uniform specifier is self-explanatory, selecting all values in the range with equal probability. |
| * The exponential specifier draws values towards the end of the range with higher probability, raising |
| * a floating point number in the range [0..1.0) to the power provided, and translating the resulting value |
| * to a uniform value in the range. |
| * The quantized exponential specifier partitions the range into {@code attempts} buckets, then applies the pure |
| * exponential approach to draw values from [0..attempts), before drawing a uniform value from the corresponding bucket |
| */ |
| public class RetryStrategy implements WaitStrategy |
| { |
| private static final Pattern RANDOMIZER = Pattern.compile( |
| "uniform|exp(onential)?[(](?<exp>[0-9.]+)[)]|q(uantized)?exp(onential)?[(](?<qexp>[0-9.]+)[)]"); |
| |
| static final Pattern PARSE = Pattern.compile( |
| "(\\s*(?<minmin>0|[0-9]+[mu]?s)\\s*<=)?" + |
| "(\\s*(?<min>[^=]+)([(]?\\s*<=\\s*(?<maxmin>0|[0-9]+[mu]?s)\\s*[)]?)?\\s*[.]{3})?" + |
| "(\\s*(?<max>[^=]+))" + |
| "(\\s*<=\\s*(?<maxmax>0|[0-9]+[mu]?s))?"); |
| |
| public static final WaitRandomizerFactory randomizers = new WaitRandomizerFactory(){}; |
| |
| public interface WaitRandomizer |
| { |
| long wait(long min, long max, int attempts); |
| } |
| |
| public static WaitRandomizerFactory randomizers(RandomSource random) |
| { |
| return new WaitRandomizerFactory() |
| { |
| @Override public LongBinaryOperator uniformLongSupplier() { return random::nextLong; } |
| @Override public DoubleSupplier uniformDoubleSupplier() { return random::nextDouble; } |
| }; |
| } |
| |
| public interface WaitRandomizerFactory |
| { |
| default LongBinaryOperator uniformLongSupplier() { return (min, max) -> ThreadLocalRandom.current().nextLong(min, max); } // DO NOT USE METHOD HANDLES (want to fetch afresh each time) |
| default DoubleSupplier uniformDoubleSupplier() { return () -> ThreadLocalRandom.current().nextDouble(); } |
| |
| default WaitRandomizer uniform() { return new Uniform(uniformLongSupplier()); } |
| default WaitRandomizer exponential(double power) { return new Exponential(uniformLongSupplier(), uniformDoubleSupplier(), power); } |
| default WaitRandomizer quantizedExponential(double power) { return new QuantizedExponential(uniformLongSupplier(), uniformDoubleSupplier(), power); } |
| |
| class Uniform implements WaitRandomizer |
| { |
| final LongBinaryOperator uniformLong; |
| |
| public Uniform(LongBinaryOperator uniformLong) |
| { |
| this.uniformLong = uniformLong; |
| } |
| |
| @Override |
| public long wait(long min, long max, int attempts) |
| { |
| return uniformLong.applyAsLong(min, max); |
| } |
| } |
| |
| abstract class AbstractExponential implements WaitRandomizer |
| { |
| final LongBinaryOperator uniformLong; |
| final DoubleSupplier uniformDouble; |
| final double power; |
| |
| public AbstractExponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) |
| { |
| this.uniformLong = uniformLong; |
| this.uniformDouble = uniformDouble; |
| this.power = power; |
| } |
| } |
| |
| class Exponential extends AbstractExponential |
| { |
| public Exponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) |
| { |
| super(uniformLong, uniformDouble, power); |
| } |
| |
| @Override |
| public long wait(long min, long max, int attempts) |
| { |
| if (attempts == 1) |
| return uniformLong.applyAsLong(min, max); |
| |
| double p = uniformDouble.getAsDouble(); |
| long delta = max - min; |
| delta *= Math.pow(p, power); |
| return max - delta; |
| } |
| } |
| |
| class QuantizedExponential extends AbstractExponential |
| { |
| public QuantizedExponential(LongBinaryOperator uniformLong, DoubleSupplier uniformDouble, double power) |
| { |
| super(uniformLong, uniformDouble, power); |
| } |
| |
| @Override |
| public long wait(long min, long max, int attempts) |
| { |
| long quanta = (max - min) / attempts; |
| if (attempts == 1 || quanta == 0) |
| return uniformLong.applyAsLong(min, max); |
| |
| double p = uniformDouble.getAsDouble(); |
| int base = (int) (attempts * Math.pow(p, power)); |
| return max - ThreadLocalRandom.current().nextLong(quanta * base, quanta * (base + 1)); |
| } |
| } |
| } |
| |
| public final WaitRandomizer waitRandomizer; |
| public final long minMinMicros, maxMinMicros, maxMaxMicros; |
| public final @Nullable Wait min; |
| public final @Nonnull Wait max; |
| public final int maxAttempts; |
| |
| protected RetryStrategy(WaitRandomizer waitRandomizer, long minMinMicros, Wait min, long maxMinMicros, Wait max, long maxMaxMicros, int retries) |
| { |
| this.waitRandomizer = waitRandomizer; |
| this.minMinMicros = minMinMicros; |
| this.min = min; |
| this.maxMinMicros = maxMinMicros; |
| this.max = max; |
| this.maxMaxMicros = maxMaxMicros; |
| this.maxAttempts = retries == Integer.MAX_VALUE ? Integer.MAX_VALUE : retries + 1; |
| Invariants.require(maxAttempts >= 1); |
| } |
| |
| public long computeWaitUntil(int attempts) |
| { |
| long wait = computeWait(attempts, NANOSECONDS); |
| if (wait < 0) |
| return -1; |
| return nanoTime() + wait; |
| } |
| |
| public long computeWait(int attempt, TimeUnit units) |
| { |
| if (attempt > maxAttempts) |
| return -1; |
| |
| long result; |
| if (min == null) |
| { |
| result = max.getMicros(attempt); |
| } |
| else |
| { |
| long min = this.min.getMicros(attempt); |
| if (min > maxMinMicros) |
| min = maxMinMicros; |
| long max = this.max.getMicros(attempt); |
| if (max > maxMaxMicros) |
| max = maxMaxMicros; |
| result = min >= max ? min : waitRandomizer.wait(min, max, attempt); |
| } |
| |
| if (result > maxMaxMicros) result = maxMaxMicros; |
| if (result < minMinMicros) result = minMinMicros; |
| return units.convert(result, MICROSECONDS); |
| } |
| |
| public long computeMaxWait(int attempt, TimeUnit units) |
| { |
| return units.convert(max.getMaxMicros(attempt), MICROSECONDS); |
| } |
| |
| public static RetryStrategy parse(String spec, LatencySourceFactory latencies) |
| { |
| return parse(spec, latencies, null); |
| } |
| |
| public static RetryStrategy parse(String spec, LatencySourceFactory latencies, WaitRandomizer randomizer) |
| { |
| String original = spec; |
| int retries = Integer.MAX_VALUE; |
| int end = spec.length(); |
| { |
| int next; |
| while ((next = spec.lastIndexOf(',', end - 1)) >= 0) |
| { |
| int mid = spec.indexOf('=', next + 1); |
| if (mid <= next || mid >= end) |
| throw new IllegalArgumentException("Invalid modifier specification: '" + spec.substring(next, end) + "'; expecting '=' for value assignment"); |
| String key = spec.substring(next + 1, mid).trim(); |
| String value = spec.substring(mid + 1, end).trim(); |
| switch (key) |
| { |
| default: throw new IllegalArgumentException("Invalid modifier specification: unrecognised property '" + key + '\''); |
| case "retries": |
| retries = Integer.parseInt(value); |
| if (retries < 0) |
| throw new IllegalArgumentException("retries must be non-negative (retries=" + value + " supplied)"); |
| break; |
| case "attempts": |
| retries = Integer.parseInt(value); |
| if (retries < 0) |
| throw new IllegalArgumentException("Must permit at least one attempt (attempts=" + value + " supplied)"); |
| break; |
| case "rnd": |
| if (randomizer != null) |
| throw new IllegalArgumentException("Randomizer already specified, cannot re-specify: " + value); |
| randomizer = parseWaitRandomizer(value); |
| break; |
| } |
| end = next; |
| } |
| if (end != spec.length()) |
| spec = spec.substring(0, end); |
| } |
| |
| Matcher m = PARSE.matcher(spec); |
| if (!m.matches()) |
| throw new IllegalArgumentException("Invalid specification: '" + spec + "'; does not match " + PARSE); |
| |
| long minMin = parseInMicros(m.group("minmin"), 0); |
| long maxMax = parseInMicros(m.group("maxmax"), Long.MAX_VALUE); |
| Wait max = TimeoutStrategy.parseWait(m.group("max"), latencies); |
| String minSpec = m.group("min"); |
| Wait min = minSpec == null ? null : TimeoutStrategy.parseWait(minSpec, latencies); |
| if (min == null && randomizer != null) |
| throw new IllegalArgumentException("Invalid to specify randomiser when no range specified: '" + original + '\''); |
| if (min instanceof Wait.Constant && minMin != 0) |
| throw new IllegalArgumentException("Invalid to specify an absolute minimum constant when the min bound is itself a constant: '" + original + '\''); |
| long maxMin = parseInMicros(m.group("maxmin"), Long.MAX_VALUE); |
| if (min instanceof Wait.Constant && maxMin != Long.MAX_VALUE) |
| throw new IllegalArgumentException("Invalid to specify an absolute max(min) constant when the min bound is itself a constant: '" + original + '\''); |
| if (max instanceof Wait.Constant && maxMax != Long.MAX_VALUE) |
| throw new IllegalArgumentException("Invalid to specify an absolute maximum constant when the max bound is itself a constant: '" + original + '\''); |
| if (randomizer == null) |
| randomizer = randomizers.uniform(); |
| return new RetryStrategy(randomizer, minMin, min, maxMin, max, maxMax, retries); |
| } |
| |
| @VisibleForTesting |
| protected static WaitRandomizer parseWaitRandomizer(String input) |
| { |
| Matcher m = RANDOMIZER.matcher(input); |
| if (!m.matches()) |
| throw new IllegalArgumentException(input + " does not match" + RANDOMIZER); |
| |
| String exp; |
| exp = m.group("exp"); |
| if (exp != null) |
| return randomizers.exponential(Double.parseDouble(exp)); |
| exp = m.group("qexp"); |
| if (exp != null) |
| return randomizers.quantizedExponential(Double.parseDouble(exp)); |
| return randomizers.uniform(); |
| } |
| } |