Skip to content

Commit

Permalink
Issue resilience4j#1043: Added IntervalBiFunction to RetryConfig to c…
Browse files Browse the repository at this point in the history
…alculate wait duration based on result/exception (resilience4j#1200)
  • Loading branch information
cosminseceleanu committed Oct 26, 2020
1 parent 2dd4adb commit 7952a61
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.resilience4j.core;

import io.vavr.control.Either;

import java.util.function.BiFunction;

/**
* An IntervalBiFunction which can be used to calculate the wait interval. The input parameters of the bi
* function is the number of attempts (attempt) and either result or exception, the output parameter is the wait interval in
* milliseconds. The attempt parameter starts at 1 and increases with every further attempt.
*/
@FunctionalInterface
public interface IntervalBiFunction<T> extends BiFunction<Integer, Either<Throwable, T>, Long> {

static <T> IntervalBiFunction<T> ofIntervalFunction(IntervalFunction f) {
return (attempt, either) -> f.apply(attempt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@ public void testCreateRetryPropertiesWithSharedConfigs() {
.createRetryConfig("backendWithDefaultConfig", compositeRetryCustomizer());
assertThat(retry1).isNotNull();
assertThat(retry1.getMaxAttempts()).isEqualTo(3);
assertThat(retry1.getIntervalFunction().apply(1)).isEqualTo(200L);
assertThat(retry1.getIntervalBiFunction().apply(1, null)).isEqualTo(200L);

// Should get shared config and overwrite wait time
RetryConfig retry2 = retryConfigurationProperties
.createRetryConfig("backendWithSharedConfig", compositeRetryCustomizer());
assertThat(retry2).isNotNull();
assertThat(retry2.getMaxAttempts()).isEqualTo(2);
assertThat(retry2.getIntervalFunction().apply(1)).isEqualTo(300L);
assertThat(retry2.getIntervalBiFunction().apply(1, null)).isEqualTo(300L);

// Unknown backend should get default config of Registry
RetryConfig retry3 = retryConfigurationProperties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
package io.github.resilience4j.retry;


import io.github.resilience4j.core.IntervalBiFunction;
import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.core.lang.Nullable;
import io.github.resilience4j.core.predicate.PredicateCreator;

import java.io.Serializable;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;

Expand All @@ -35,6 +37,7 @@ public class RetryConfig implements Serializable {
public static final long DEFAULT_WAIT_DURATION = 500;
public static final int DEFAULT_MAX_ATTEMPTS = 3;
private static final IntervalFunction DEFAULT_INTERVAL_FUNCTION = numOfAttempts -> DEFAULT_WAIT_DURATION;
private static final IntervalBiFunction DEFAULT_INTERVAL_BI_FUNCTION = IntervalBiFunction.ofIntervalFunction(DEFAULT_INTERVAL_FUNCTION);
private static final Predicate<Throwable> DEFAULT_RECORD_FAILURE_PREDICATE = throwable -> true;

@SuppressWarnings("unchecked")
Expand All @@ -48,7 +51,11 @@ public class RetryConfig implements Serializable {
private Predicate retryOnResultPredicate;

private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
private IntervalFunction intervalFunction = DEFAULT_INTERVAL_FUNCTION;

@Nullable
private IntervalFunction intervalFunction;

private IntervalBiFunction intervalBiFunction = DEFAULT_INTERVAL_BI_FUNCTION;

// The final exception predicate
private Predicate<Throwable> exceptionPredicate;
Expand Down Expand Up @@ -86,10 +93,26 @@ public int getMaxAttempts() {
return maxAttempts;
}

/**
* Use {@link RetryConfig#intervalBiFunction} instead, this method is kept for backwards compatibility
*/
@Nullable
@Deprecated
public Function<Integer, Long> getIntervalFunction() {
return intervalFunction;
}

/**
* Return the IntervalBiFunction which calculates wait interval based on result or exception
*
* @param <T> The type of result.
* @return the interval bi function
*/
@SuppressWarnings("unchecked")
public <T> IntervalBiFunction<T> getIntervalBiFunction() {
return intervalBiFunction;
}

public Predicate<Throwable> getExceptionPredicate() {
return exceptionPredicate;
}
Expand All @@ -110,13 +133,18 @@ public <T> Predicate<T> getResultPredicate() {
public static class Builder<T> {

private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
private IntervalFunction intervalFunction = IntervalFunction.ofDefaults();

@Nullable
private IntervalFunction intervalFunction;

@Nullable
private Predicate<Throwable> retryOnExceptionPredicate;
@Nullable
private Predicate<T> retryOnResultPredicate;

@Nullable
private IntervalBiFunction<T> intervalBiFunction;

@SuppressWarnings("unchecked")
private Class<? extends Throwable>[] retryExceptions = new Class[0];
@SuppressWarnings("unchecked")
Expand All @@ -129,11 +157,15 @@ public Builder() {
@SuppressWarnings("unchecked")
public Builder(RetryConfig baseConfig) {
this.maxAttempts = baseConfig.maxAttempts;
this.intervalFunction = baseConfig.intervalFunction;
this.retryOnExceptionPredicate = baseConfig.retryOnExceptionPredicate;
this.retryOnResultPredicate = baseConfig.retryOnResultPredicate;
this.retryExceptions = baseConfig.retryExceptions;
this.ignoreExceptions = baseConfig.ignoreExceptions;
if (baseConfig.intervalFunction != null) {
this.intervalFunction = baseConfig.intervalFunction;
} else {
this.intervalBiFunction = baseConfig.intervalBiFunction;
}
}

public Builder<T> maxAttempts(int maxAttempts) {
Expand All @@ -147,10 +179,10 @@ public Builder<T> maxAttempts(int maxAttempts) {

public Builder<T> waitDuration(Duration waitDuration) {
if (waitDuration.toMillis() >= 0) {
this.intervalFunction = (x) -> waitDuration.toMillis();
this.intervalBiFunction = (attempt, either) -> waitDuration.toMillis();
} else {
throw new IllegalArgumentException(
"waitDurationInOpenState must be a positive value");
"waitDuration must be a positive value");
}
return this;
}
Expand Down Expand Up @@ -179,6 +211,17 @@ public Builder<T> intervalFunction(IntervalFunction f) {
return this;
}

/**
* Set a function to modify the waiting interval after a failure based on attempt number and result or exception.
*
* @param f Function to modify the interval after a failure
* @return the RetryConfig.Builder
*/
public Builder<T> intervalBiFunction(IntervalBiFunction<T> f) {
this.intervalBiFunction = f;
return this;
}

/**
* Configures a Predicate which evaluates if an exception should be retried. The Predicate
* must return true if the exception should be retried, otherwise it must return false.
Expand Down Expand Up @@ -243,17 +286,31 @@ public final Builder<T> ignoreExceptions(
}

public RetryConfig build() {
if (intervalFunction != null && intervalBiFunction != null) {
throw new IllegalStateException("The intervalFunction was configured twice which could result in an" +
" undesired state. Please use either intervalFunction or intervalBiFunction.");
}
RetryConfig config = new RetryConfig();
config.intervalFunction = intervalFunction;
config.maxAttempts = maxAttempts;
config.retryOnExceptionPredicate = retryOnExceptionPredicate;
config.retryOnResultPredicate = retryOnResultPredicate;
config.retryExceptions = retryExceptions;
config.ignoreExceptions = ignoreExceptions;
config.exceptionPredicate = createExceptionPredicate();
config.intervalFunction = createIntervalFunction();
config.intervalBiFunction = Optional.ofNullable(intervalBiFunction)
.orElse(IntervalBiFunction.ofIntervalFunction(config.intervalFunction));
return config;
}

@Nullable
private IntervalFunction createIntervalFunction() {
if (intervalFunction == null && intervalBiFunction == null) {
return IntervalFunction.ofDefaults();
}
return intervalFunction;
}

private Predicate<Throwable> createExceptionPredicate() {
return createRetryOnExceptionPredicate()
.and(PredicateCreator.createNegatedExceptionsPredicate(ignoreExceptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
import io.github.resilience4j.retry.MaxRetriesExceeded;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.core.IntervalBiFunction;
import io.github.resilience4j.retry.event.*;
import io.vavr.CheckedConsumer;
import io.vavr.collection.HashMap;
import io.vavr.collection.Map;
import io.vavr.control.Either;
import io.vavr.control.Option;
import io.vavr.control.Try;

Expand All @@ -36,7 +38,6 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

Expand All @@ -53,7 +54,7 @@ public class RetryImpl<T> implements Retry {
private final Map<String, String> tags;

private final int maxAttempts;
private final Function<Integer, Long> intervalFunction;
private final IntervalBiFunction<T> intervalBiFunction;
private final Predicate<Throwable> exceptionPredicate;
private final LongAdder succeededAfterRetryCounter;
private final LongAdder failedAfterRetryCounter;
Expand All @@ -69,7 +70,7 @@ public RetryImpl(String name, RetryConfig config, Map<String, String> tags) {
this.config = config;
this.tags = tags;
this.maxAttempts = config.getMaxAttempts();
this.intervalFunction = config.getIntervalFunction();
this.intervalBiFunction = config.getIntervalBiFunction();
this.exceptionPredicate = config.getExceptionPredicate();
this.resultPredicate = config.getResultPredicate();
this.metrics = this.new RetryMetrics();
Expand Down Expand Up @@ -176,7 +177,7 @@ public boolean onResult(T result) {
if (currentNumOfAttempts >= maxAttempts) {
return false;
} else {
waitIntervalAfterFailure(currentNumOfAttempts, null);
waitIntervalAfterFailure(currentNumOfAttempts, Either.right(result));
return true;
}
}
Expand Down Expand Up @@ -216,7 +217,7 @@ private void throwOrSleepAfterException() throws Exception {
() -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable));
throw throwable;
} else {
waitIntervalAfterFailure(currentNumOfAttempts, throwable);
waitIntervalAfterFailure(currentNumOfAttempts, Either.left(throwable));
}
}

Expand All @@ -229,16 +230,15 @@ private void throwOrSleepAfterRuntimeException() {
() -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable));
throw throwable;
} else {
waitIntervalAfterFailure(currentNumOfAttempts, throwable);
waitIntervalAfterFailure(currentNumOfAttempts, Either.left(throwable));
}
}

private void waitIntervalAfterFailure(int currentNumOfAttempts,
@Nullable Throwable throwable) {
private void waitIntervalAfterFailure(int currentNumOfAttempts, Either<Throwable, T> either) {
// wait interval until the next attempt should start
long interval = intervalFunction.apply(numOfAttempts.get());
long interval = intervalBiFunction.apply(numOfAttempts.get(), either);
publishRetryEvent(
() -> new RetryOnRetryEvent(getName(), currentNumOfAttempts, throwable, interval));
() -> new RetryOnRetryEvent(getName(), currentNumOfAttempts, either.swap().getOrNull(), interval));
Try.run(() -> sleepFunction.accept(interval))
.getOrElseThrow(ex -> lastRuntimeException.get());
}
Expand Down Expand Up @@ -310,7 +310,7 @@ private long handleOnError(Throwable throwable) {
return -1;
}

long interval = intervalFunction.apply(attempt);
long interval = intervalBiFunction.apply(attempt, Either.left(throwable));
publishRetryEvent(() -> new RetryOnRetryEvent(getName(), attempt, throwable, interval));
return interval;
}
Expand All @@ -322,7 +322,7 @@ public long onResult(T result) {
if (attempt >= maxAttempts) {
return -1;
}
return intervalFunction.apply(attempt);
return intervalBiFunction.apply(attempt, Either.right(result));
} else {
return -1;
}
Expand Down
Loading

0 comments on commit 7952a61

Please sign in to comment.