blob: bc453f5bea6ebb5274fbe809222af58201848a0b [file] [log] [blame]
/*
* 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.ratis.retry;
import org.apache.ratis.util.JavaUtils;
import org.apache.ratis.util.TimeDuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Given pairs of number of retries and sleep time (n0, t0), (n1, t1), ...,
* the first n0 retries sleep t0 milliseconds on average,
* the following n1 retries sleep t1 milliseconds on average, and so on.
*
* For all the sleep, the actual sleep time is randomly uniform distributed
* in the close interval [0.5t, 1.5t], where t is the sleep time specified.
*
* The objects of this class are immutable.
*/
public final class MultipleLinearRandomRetry implements RetryPolicy {
static final Logger LOG = LoggerFactory.getLogger(MultipleLinearRandomRetry.class);
/** Pairs of numRetries and sleepSeconds */
private static class Pair {
private final int numRetries;
private final TimeDuration sleepTime;
Pair(int numRetries, TimeDuration sleepTime) {
if (numRetries < 0) {
throw new IllegalArgumentException("numRetries = " + numRetries+" < 0");
}
if (sleepTime.isNegative()) {
throw new IllegalArgumentException("sleepTime = " + sleepTime + " < 0");
}
this.numRetries = numRetries;
this.sleepTime = sleepTime;
}
TimeDuration getRandomSleepTime() {
// 0.5 <= ratio < 1.5
final double ratio = ThreadLocalRandom.current().nextDouble() + 0.5;
return sleepTime.multiply(ratio);
}
@Override
public String toString() {
return numRetries + "x" + sleepTime;
}
}
private final List<Pair> pairs;
private final Supplier<String> myString;
private MultipleLinearRandomRetry(List<Pair> pairs) {
if (pairs == null || pairs.isEmpty()) {
throw new IllegalArgumentException("pairs must be neither null nor empty.");
}
this.pairs = Collections.unmodifiableList(pairs);
this.myString = JavaUtils.memoize(() -> JavaUtils.getClassSimpleName(getClass()) + pairs);
}
@Override
public Action handleAttemptFailure(Event event) {
final Pair p = searchPair(event.getAttemptCount());
return p == null? NO_RETRY_ACTION: p::getRandomSleepTime;
}
/**
* Given the current number of retry, search the corresponding pair.
* @return the corresponding pair,
* or null if the current number of retry > maximum number of retry.
*/
private Pair searchPair(int curRetry) {
int i = 0;
for(; i < pairs.size() && curRetry > pairs.get(i).numRetries; i++) {
curRetry -= pairs.get(i).numRetries;
}
return i == pairs.size()? null: pairs.get(i);
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public boolean equals(final Object that) {
if (this == that) {
return true;
} else if (that == null || this.getClass() != that.getClass()) {
return false;
}
return this.toString().equals(that.toString());
}
@Override
public String toString() {
return myString.get();
}
/**
* Parse the given string as a MultipleLinearRandomRetry object.
* The format of the string is "t_1, n_1, t_2, n_2, ...",
* where t_i and n_i are the i-th pair of sleep time and number of retries.
* Note that the white spaces in the string are ignored.
*
* @return the parsed object, or null if the parsing fails.
*/
public static MultipleLinearRandomRetry parseCommaSeparated(String input) {
final String[] elements = input.split(",");
if (elements.length == 0) {
LOG.warn("Illegal value: there is no element in \"{}\".", input);
return null;
}
if (elements.length % 2 != 0) {
LOG.warn("Illegal value: the number of elements in \"{}\" is {} but an even number of elements is expected.",
input, elements.length);
return null;
}
final List<Pair> pairs = new ArrayList<>();
for(int i = 0; i < elements.length; ) {
//parse the i-th sleep-time
final TimeDuration sleep = parseElement(elements, i++, input, MultipleLinearRandomRetry::parsePositiveTime);
if (sleep == null) {
return null; //parse fails
}
//parse the i-th number-of-retries
final Integer retries = parseElement(elements, i++, input, MultipleLinearRandomRetry::parsePositiveInt);
if (retries == null) {
return null; //parse fails
}
pairs.add(new Pair(retries, sleep));
}
return new MultipleLinearRandomRetry(pairs);
}
private static TimeDuration parsePositiveTime(String s) {
final TimeDuration t = TimeDuration.valueOf(s, TimeUnit.MILLISECONDS);
if (t.isNonPositive()) {
throw new IllegalArgumentException("Non-positive value: " + t);
}
return t;
}
private static int parsePositiveInt(String trimmed) {
final int n = Integer.parseInt(trimmed);
if (n <= 0) {
throw new IllegalArgumentException("Non-positive value: " + n);
}
return n;
}
private static <E> E parseElement(String[] elements, int i, String input, Function<String, E> parser) {
final String s = elements[i].trim().replace("_", "");
try {
return parser.apply(s);
} catch(Exception t) {
LOG.warn("Failed to parse \"{}\", which is the index {} element in \"{}\"", s, i, input, t);
return null;
}
}
}