/*
* 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 gobblin.util;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import lombok.Builder;
/**
* Utility class for exponential backoff.
*
* Usage:
* ExponentialBackoff exponentialBackoff = ExponentialBackoff.builder().build();
* exponentialBackoff.awaitNextRetry();
*/
public class ExponentialBackoff {
private final double alpha;
private final long maxWait;
private final int maxRetries;
private final long maxDelay;
private int retryNumber = 0;
private long totalWait = 0;
private long nextDelay;
/**
* @param alpha the multiplier for each backoff iteration. (def: 2)
* @param maxRetries maximum number of retries allowed. (def: infinite)
* @param maxWait maximum wait allowed in millis. (def: infinite)
* @param maxDelay maximum delay allowed in millis. (def: infinite)
* @param initialDelay initial delay in millis. (def: 20)
*/
@Builder
private ExponentialBackoff(Double alpha, Integer maxRetries, Long maxWait, Long maxDelay, Long initialDelay) {
this.alpha = alpha == null ? 2 : alpha;
this.maxRetries = maxRetries == null ? Integer.MAX_VALUE : maxRetries;
this.maxWait = maxWait == null ? Long.MAX_VALUE : maxWait;
this.maxDelay = maxDelay == null ? Long.MAX_VALUE : maxDelay;
this.nextDelay = initialDelay == null ? 20 : initialDelay;
}
/**
* Block until next retry can be executed.
*
* This method throws an exception if the max number of retries has been reached. For an alternative see
* {@link #awaitNextRetryIfAvailable()}.
*
* @throws NoMoreRetriesException If maximum number of retries has been reached.
*/
public void awaitNextRetry() throws InterruptedException, NoMoreRetriesException {
this.retryNumber++;
if (this.retryNumber > this.maxRetries) {
throw new NoMoreRetriesException("Reached maximum number of retries: " + this.maxRetries);
} else if (this.totalWait > this.maxWait) {
throw new NoMoreRetriesException("Reached maximum time to wait: " + this.maxWait);
}
Thread.sleep(this.nextDelay);
this.totalWait += this.nextDelay;
this.nextDelay = Math.min((long) (this.alpha * this.nextDelay) + 1, this.maxDelay);
}
/**
* Block until next retry can be executed unless max retries has been reached.
*
* This method uses the return value to specify if a retry is allowed, which the caller should respect. For an alternative see
* {@link #awaitNextRetry()}.
*
* @return true if the next execution can be run, false if the max number of retries has been reached.
*/
public boolean awaitNextRetryIfAvailable() throws InterruptedException {
try {
awaitNextRetry();
return true;
} catch (NoMoreRetriesException exc) {
return false;
}
}
/**
* Thrown if no more retries are available for {@link ExponentialBackoff}.
*/
public static class NoMoreRetriesException extends Exception {
public NoMoreRetriesException(String message) {
super(message);
}
}
/**
* Evaluate a condition until true with exponential backoff.
* @param callable Condition.
* @return true if the condition returned true.
* @throws ExecutionException if the condition throws an exception.
*/
@Builder(builderMethodName = "awaitCondition", buildMethodName = "await")
private static boolean evaluateConditionUntilTrue(Callable<Boolean> callable, Double alpha, Integer maxRetries,
Long maxWait, Long maxDelay, Long initialDelay) throws ExecutionException, InterruptedException {
ExponentialBackoff exponentialBackoff = new ExponentialBackoff(alpha, maxRetries, maxWait, maxDelay, initialDelay);
while (true) {
try {
if (callable.call()) {
return true;
}
} catch (Throwable t) {
throw new ExecutionException(t);
}
if (!exponentialBackoff.awaitNextRetryIfAvailable()) {
return false;
}
}
}
}