Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add @RetryWhen #1007

Merged
merged 1 commit into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.smallrye.faulttolerance.api;

import java.util.function.Predicate;

public final class AlwaysOnException implements Predicate<Throwable> {
@Override
public boolean test(Throwable ignored) {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,24 @@ default RetryBuilder<T, R> abortOn(Class<? extends Throwable> value) {
return abortOn(Collections.singleton(Objects.requireNonNull(value)));
}

/**
* Sets a predicate to determine when a result should be considered failure and retry
* should be attempted. All results that do not match this predicate are implicitly
* considered success.
*
* @param value the predicate, must not be {@code null}
* @return this retry builder
*/
RetryBuilder<T, R> whenResult(Predicate<Object> value);

/**
* @deprecated use {@link #whenException(Predicate)}
*/
@Deprecated(forRemoval = true)
default RetryBuilder<T, R> when(Predicate<Throwable> value) {
return whenException(value);
}

/**
* Sets a predicate to determine when an exception should be considered failure
* and retry should be attempted. This is a more general variant of {@link #retryOn(Collection) retryOn}.
Expand All @@ -847,9 +865,9 @@ default RetryBuilder<T, R> abortOn(Class<? extends Throwable> value) {
* If this method is called, {@code retryOn} and {@code abortOn} may not be called.
*
* @param value the predicate, must not be {@code null}
* @return this fallback builder
* @return this retry builder
*/
RetryBuilder<T, R> when(Predicate<Throwable> value);
RetryBuilder<T, R> whenException(Predicate<Throwable> value);

/**
* Configures retry to use an exponential backoff instead of the default constant backoff.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.smallrye.faulttolerance.api;

import java.util.function.Predicate;

public final class NeverOnResult implements Predicate<Object> {
@Override
public boolean test(Object ignored) {
return false;
}
}
53 changes: 53 additions & 0 deletions api/src/main/java/io/smallrye/faulttolerance/api/RetryWhen.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.smallrye.faulttolerance.api;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.function.Predicate;

import io.smallrye.common.annotation.Experimental;

/**
* Modifies a {@code @Retry} annotation to retry when given predicate returns {@code true}.
* May only be present on elements that are also annotated {@code @Retry}. If this annotation
* is present and the {@code @RetryWhen.exception} member is set, the {@code @Retry.retryOn}
* and {@code @Retry.abortOn} members must not be set.
* <p>
* For each usage of the {@code @RetryWhen} annotation, all given {@link Predicate}s are
* instantiated once. The predicate classes must have a {@code public}, zero-parameter
* constructor. They must be thread-safe, ideally stateless.
*
* @see #exception()
* @see #result()
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@Experimental("first attempt at providing predicate-based retries")
public @interface RetryWhen {
/**
* Class of the predicate that will be used to determine whether the action should be retried
* if the action has returned a result.
* <p>
* Even if the guarded action is asynchronous, the predicate takes a produced result.
* The predicate is never passed a {@link java.util.concurrent.CompletionStage CompletionStage} or so.
*
* @return predicate class
*/
Class<? extends Predicate<Object>> result() default NeverOnResult.class;

/**
* Class of the predicate that will be used to determine whether the action should be retried
* if the action has thrown an exception.
* <p>
* Even if the guarded action is asynchronous, the predicate takes a produced exception.
* The predicate is never passed a {@link java.util.concurrent.CompletionStage CompletionStage} or so.
*
* @return predicate class
*/
Class<? extends Predicate<Throwable>> exception() default AlwaysOnException.class;
}
55 changes: 54 additions & 1 deletion doc/modules/ROOT/pages/reference/retry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ This configuration takes precedence over `retryOn`.
[[metrics]]
== Metrics

Rate limit exposes the following metrics:
Retry exposes the following metrics:

[cols="1,5"]
|===
Expand Down Expand Up @@ -183,6 +183,59 @@ This is an advanced option.

For more information about these backoff strategies, see the javadoc of the annotations.

=== Predicate-Based `@Retry`

include::partial$srye-feature.adoc[]

By default, an operation is retried only if it fails and the exception is assignable to one of the classes defined in `@Retry.retryOn` (and not assignable to any of the classes defined in `@Retry.abortOn`).
With the `@RetryWhen` annotation, it is possible to instead define a custom predicate for the exception, as well as a predicate for the result.

The `@RetryWhen` annotation may be present on any program element (method or class) that also has the `@Retry` annotation.
For example:

[source,java]
----
package com.example;

@ApplicationScoped
public class MyService {
@Retry
@RetryWhen(
result = IsNull.class, // <1>
exception = IsRuntimeException.class // <2>
)
public String hello() {
...
}

public static final class IsNull implements Predicate<Object> {
@Override
public boolean test(Object o) {
return o == null;
}
}

public static final class IsRuntimeException implements Predicate<Throwable> {
@Override
public boolean test(Throwable throwable) {
return throwable instanceof RuntimeException;
}
}
}
----

<1> When the method returns `null`, it will be retried.
<2> When the method throws a `RuntimeException`, it will be retried.

All other results are considered expected and are not retried.

It is an error to specify `@RetryWhen.exception` if `@Retry.retryOn` / `@Retry.abortOn` is specified.
Specifying `@RetryWhen.result` together with `@Retry.retryOn` / `@Retry.abortOn` is possible, although perhaps not the best idea.

It is an error to add a `@RetryWhen` annotation to a program element that doesn't have `@Retry` (e.g. add `@Retry` on a class and `@RetryWhen` on a method).

For more information about `@RetryWhen`, see the javadoc of the annotation.

[[inspecting-exception-cause-chains]]
=== Inspecting Exception Cause Chains

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.smallrye.faulttolerance.api.ExponentialBackoff;
import io.smallrye.faulttolerance.api.FibonacciBackoff;
import io.smallrye.faulttolerance.api.RateLimit;
import io.smallrye.faulttolerance.api.RetryWhen;

/**
* Created in the CDI extension to capture effective annotations for each
Expand Down Expand Up @@ -57,6 +58,7 @@ public class FaultToleranceMethod {
public CustomBackoff customBackoff;
public ExponentialBackoff exponentialBackoff;
public FibonacciBackoff fibonacciBackoff;
public RetryWhen retryWhen;

// types of annotations that were declared directly on the method;
// other annotations, if present, were declared on the type
Expand All @@ -67,7 +69,7 @@ public boolean isLegitimate() {
return false;
}

// certain SmallRye annotations (@CircuitBreakerName, @[Non]Blocking, @*Backoff)
// certain SmallRye annotations (@CircuitBreakerName, @[Non]Blocking, @*Backoff, @RetryWhen)
// do _not_ trigger the fault tolerance interceptor alone, only in combination
// with other fault tolerance annotations
return applyFaultTolerance != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
import io.smallrye.faulttolerance.core.util.ExceptionDecision;
import io.smallrye.faulttolerance.core.util.Preconditions;
import io.smallrye.faulttolerance.core.util.PredicateBasedExceptionDecision;
import io.smallrye.faulttolerance.core.util.PredicateBasedResultDecision;
import io.smallrye.faulttolerance.core.util.ResultDecision;
import io.smallrye.faulttolerance.core.util.SetBasedExceptionDecision;
import io.smallrye.faulttolerance.core.util.SetOfThrowables;

Expand Down Expand Up @@ -306,7 +308,9 @@ private FaultToleranceStrategy<T> buildSyncStrategy(BuilderLazyDependencies lazy
Supplier<BackOff> backoff = prepareRetryBackoff(retryBuilder);

result = new Retry<>(result, description,
createExceptionDecision(retryBuilder.abortOn, retryBuilder.retryOn, retryBuilder.whenPredicate),
createResultDecision(retryBuilder.whenResultPredicate),
createExceptionDecision(retryBuilder.abortOn, retryBuilder.retryOn,
retryBuilder.whenExceptionPredicate),
retryBuilder.maxRetries, retryBuilder.maxDurationInMillis, () -> new ThreadSleepDelay(backoff.get()),
SystemStopwatch.INSTANCE);
}
Expand Down Expand Up @@ -377,7 +381,9 @@ private <V> FaultToleranceStrategy<CompletionStage<V>> buildAsyncStrategy(Builde
Supplier<BackOff> backoff = prepareRetryBackoff(retryBuilder);

result = new CompletionStageRetry<>(result, description,
createExceptionDecision(retryBuilder.abortOn, retryBuilder.retryOn, retryBuilder.whenPredicate),
createResultDecision(retryBuilder.whenResultPredicate),
createExceptionDecision(retryBuilder.abortOn, retryBuilder.retryOn,
retryBuilder.whenExceptionPredicate),
retryBuilder.maxRetries, retryBuilder.maxDurationInMillis,
() -> new TimerDelay(backoff.get(), lazyDependencies.timer()),
SystemStopwatch.INSTANCE);
Expand Down Expand Up @@ -418,13 +424,23 @@ private MeteredOperation buildMeteredOperation() {
retryBuilder != null, timeoutBuilder != null);
}

private static ResultDecision createResultDecision(Predicate<Object> whenResultPredicate) {
if (whenResultPredicate != null) {
// the builder API accepts a predicate that returns `true` when a result is considered failure,
// but `[CompletionStage]Retry` accepts a predicate that returns `true` when a result is
// considered success -- hence the negation
return new PredicateBasedResultDecision(whenResultPredicate.negate());
}
return ResultDecision.ALWAYS_EXPECTED;
}

private static ExceptionDecision createExceptionDecision(Collection<Class<? extends Throwable>> consideredExpected,
Collection<Class<? extends Throwable>> consideredFailure, Predicate<Throwable> whenPredicate) {
if (whenPredicate != null) {
Collection<Class<? extends Throwable>> consideredFailure, Predicate<Throwable> whenExceptionPredicate) {
if (whenExceptionPredicate != null) {
// the builder API accepts a predicate that returns `true` when an exception is considered failure,
// but `PredicateBasedExceptionDecision` accepts a predicate that returns `true` when an exception
// is considered success -- hence the negation
return new PredicateBasedExceptionDecision(whenPredicate.negate());
return new PredicateBasedExceptionDecision(whenExceptionPredicate.negate());
}
return new SetBasedExceptionDecision(createSetOfThrowables(consideredFailure),
createSetOfThrowables(consideredExpected), true);
Expand Down Expand Up @@ -756,7 +772,8 @@ static class RetryBuilderImpl<T, R> implements RetryBuilder<T, R> {
private Collection<Class<? extends Throwable>> retryOn = Collections.singleton(Exception.class);
private Collection<Class<? extends Throwable>> abortOn = Collections.emptySet();
private boolean setBasedExceptionDecisionDefined = false;
private Predicate<Throwable> whenPredicate;
private Predicate<Throwable> whenExceptionPredicate;
private Predicate<Object> whenResultPredicate;

private ExponentialBackoffBuilderImpl<T, R> exponentialBackoffBuilder;
private FibonacciBackoffBuilderImpl<T, R> fibonacciBackoffBuilder;
Expand Down Expand Up @@ -818,8 +835,14 @@ public RetryBuilder<T, R> abortOn(Collection<Class<? extends Throwable>> value)
}

@Override
public RetryBuilder<T, R> when(Predicate<Throwable> value) {
this.whenPredicate = Preconditions.checkNotNull(value, "Exception predicate must be set");
public RetryBuilder<T, R> whenResult(Predicate<Object> value) {
this.whenResultPredicate = Preconditions.checkNotNull(value, "Result predicate must be set");
return this;
}

@Override
public RetryBuilder<T, R> whenException(Predicate<Throwable> value) {
this.whenExceptionPredicate = Preconditions.checkNotNull(value, "Exception predicate must be set");
return this;
}

Expand Down Expand Up @@ -858,8 +881,8 @@ public RetryBuilder<T, R> onFailure(Runnable callback) {

@Override
public Builder<T, R> done() {
if (whenPredicate != null && setBasedExceptionDecisionDefined) {
throw new IllegalStateException("The when() method may not be combined with retryOn() / abortOn()");
if (whenExceptionPredicate != null && setBasedExceptionDecisionDefined) {
throw new IllegalStateException("The whenException() method may not be combined with retryOn()/abortOn()");
}

int backoffStrategies = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
import io.smallrye.faulttolerance.core.stopwatch.RunningStopwatch;
import io.smallrye.faulttolerance.core.stopwatch.Stopwatch;
import io.smallrye.faulttolerance.core.util.ExceptionDecision;
import io.smallrye.faulttolerance.core.util.ResultDecision;

public class CompletionStageRetry<V> extends Retry<CompletionStage<V>> {
private final Supplier<AsyncDelay> delayBetweenRetries;

public CompletionStageRetry(FaultToleranceStrategy<CompletionStage<V>> delegate, String description,
ExceptionDecision exceptionDecision, long maxRetries, long maxTotalDurationInMillis,
Supplier<AsyncDelay> delayBetweenRetries, Stopwatch stopwatch) {
ResultDecision resultDecision, ExceptionDecision exceptionDecision, long maxRetries,
long maxTotalDurationInMillis, Supplier<AsyncDelay> delayBetweenRetries, Stopwatch stopwatch) {
// the SyncDelay.NONE is ignored here, we have our own AsyncDelay
super(delegate, description, exceptionDecision, maxRetries, maxTotalDurationInMillis, SyncDelay.NONE, stopwatch);
super(delegate, description, resultDecision, exceptionDecision, maxRetries, maxTotalDurationInMillis,
SyncDelay.NONE, stopwatch);
this.delayBetweenRetries = checkNotNull(delayBetweenRetries, "Delay must be set");
}

Expand Down Expand Up @@ -87,10 +89,14 @@ private CompletionStage<V> afterDelay(InvocationContext<CompletionStage<V>> ctx,

delegate.apply(ctx).whenComplete((value, exception) -> {
if (exception == null) {
ctx.fireEvent(RetryEvents.Finished.VALUE_RETURNED);
result.complete(value);
if (shouldAbortRetryingOnResult(value)) {
ctx.fireEvent(RetryEvents.Finished.VALUE_RETURNED);
result.complete(value);
} else {
propagateCompletion(doRetry(ctx, attempt + 1, delay, stopwatch, exception), result);
}
} else {
if (shouldAbortRetrying(exception)) {
if (shouldAbortRetryingOnException(exception)) {
ctx.fireEvent(RetryEvents.Finished.EXCEPTION_NOT_RETRYABLE);
result.completeExceptionally(exception);
} else {
Expand All @@ -101,7 +107,7 @@ private CompletionStage<V> afterDelay(InvocationContext<CompletionStage<V>> ctx,

return result;
} catch (Throwable e) {
if (shouldAbortRetrying(e)) {
if (shouldAbortRetryingOnException(e)) {
ctx.fireEvent(RetryEvents.Finished.EXCEPTION_NOT_RETRYABLE);
return failedFuture(e);
} else {
Expand Down
Loading
Loading