diff --git a/api/src/main/java/io/smallrye/faulttolerance/api/AlwaysOnException.java b/api/src/main/java/io/smallrye/faulttolerance/api/AlwaysOnException.java new file mode 100644 index 000000000..33b7a4bcd --- /dev/null +++ b/api/src/main/java/io/smallrye/faulttolerance/api/AlwaysOnException.java @@ -0,0 +1,10 @@ +package io.smallrye.faulttolerance.api; + +import java.util.function.Predicate; + +public final class AlwaysOnException implements Predicate { + @Override + public boolean test(Throwable ignored) { + return true; + } +} diff --git a/api/src/main/java/io/smallrye/faulttolerance/api/FaultTolerance.java b/api/src/main/java/io/smallrye/faulttolerance/api/FaultTolerance.java index 993fd1cd3..2be3ce947 100644 --- a/api/src/main/java/io/smallrye/faulttolerance/api/FaultTolerance.java +++ b/api/src/main/java/io/smallrye/faulttolerance/api/FaultTolerance.java @@ -838,6 +838,24 @@ default RetryBuilder abortOn(Class 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 whenResult(Predicate value); + + /** + * @deprecated use {@link #whenException(Predicate)} + */ + @Deprecated(forRemoval = true) + default RetryBuilder when(Predicate 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}. @@ -847,9 +865,9 @@ default RetryBuilder abortOn(Class 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 when(Predicate value); + RetryBuilder whenException(Predicate value); /** * Configures retry to use an exponential backoff instead of the default constant backoff. diff --git a/api/src/main/java/io/smallrye/faulttolerance/api/NeverOnResult.java b/api/src/main/java/io/smallrye/faulttolerance/api/NeverOnResult.java new file mode 100644 index 000000000..4622ed6c6 --- /dev/null +++ b/api/src/main/java/io/smallrye/faulttolerance/api/NeverOnResult.java @@ -0,0 +1,10 @@ +package io.smallrye.faulttolerance.api; + +import java.util.function.Predicate; + +public final class NeverOnResult implements Predicate { + @Override + public boolean test(Object ignored) { + return false; + } +} diff --git a/api/src/main/java/io/smallrye/faulttolerance/api/RetryWhen.java b/api/src/main/java/io/smallrye/faulttolerance/api/RetryWhen.java new file mode 100644 index 000000000..d2f40cd75 --- /dev/null +++ b/api/src/main/java/io/smallrye/faulttolerance/api/RetryWhen.java @@ -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. + *

+ * 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. + *

+ * 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> 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. + *

+ * 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> exception() default AlwaysOnException.class; +} diff --git a/doc/modules/ROOT/pages/reference/retry.adoc b/doc/modules/ROOT/pages/reference/retry.adoc index 317c098f3..6fe81d0ef 100644 --- a/doc/modules/ROOT/pages/reference/retry.adoc +++ b/doc/modules/ROOT/pages/reference/retry.adoc @@ -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"] |=== @@ -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 { + @Override + public boolean test(Object o) { + return o == null; + } + } + + public static final class IsRuntimeException implements Predicate { + @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 diff --git a/implementation/autoconfig/core/src/main/java/io/smallrye/faulttolerance/autoconfig/FaultToleranceMethod.java b/implementation/autoconfig/core/src/main/java/io/smallrye/faulttolerance/autoconfig/FaultToleranceMethod.java index 21c2ca9a3..9ed7d36e3 100644 --- a/implementation/autoconfig/core/src/main/java/io/smallrye/faulttolerance/autoconfig/FaultToleranceMethod.java +++ b/implementation/autoconfig/core/src/main/java/io/smallrye/faulttolerance/autoconfig/FaultToleranceMethod.java @@ -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 @@ -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 @@ -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 diff --git a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/apiimpl/FaultToleranceImpl.java b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/apiimpl/FaultToleranceImpl.java index 7038d3ee8..bba3c0830 100644 --- a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/apiimpl/FaultToleranceImpl.java +++ b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/apiimpl/FaultToleranceImpl.java @@ -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; @@ -306,7 +308,9 @@ private FaultToleranceStrategy buildSyncStrategy(BuilderLazyDependencies lazy Supplier 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); } @@ -377,7 +381,9 @@ private FaultToleranceStrategy> buildAsyncStrategy(Builde Supplier 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); @@ -418,13 +424,23 @@ private MeteredOperation buildMeteredOperation() { retryBuilder != null, timeoutBuilder != null); } + private static ResultDecision createResultDecision(Predicate 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> consideredExpected, - Collection> consideredFailure, Predicate whenPredicate) { - if (whenPredicate != null) { + Collection> consideredFailure, Predicate 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); @@ -756,7 +772,8 @@ static class RetryBuilderImpl implements RetryBuilder { private Collection> retryOn = Collections.singleton(Exception.class); private Collection> abortOn = Collections.emptySet(); private boolean setBasedExceptionDecisionDefined = false; - private Predicate whenPredicate; + private Predicate whenExceptionPredicate; + private Predicate whenResultPredicate; private ExponentialBackoffBuilderImpl exponentialBackoffBuilder; private FibonacciBackoffBuilderImpl fibonacciBackoffBuilder; @@ -818,8 +835,14 @@ public RetryBuilder abortOn(Collection> value) } @Override - public RetryBuilder when(Predicate value) { - this.whenPredicate = Preconditions.checkNotNull(value, "Exception predicate must be set"); + public RetryBuilder whenResult(Predicate value) { + this.whenResultPredicate = Preconditions.checkNotNull(value, "Result predicate must be set"); + return this; + } + + @Override + public RetryBuilder whenException(Predicate value) { + this.whenExceptionPredicate = Preconditions.checkNotNull(value, "Exception predicate must be set"); return this; } @@ -858,8 +881,8 @@ public RetryBuilder onFailure(Runnable callback) { @Override public Builder 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; diff --git a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetry.java b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetry.java index 2fc70db8f..22427ed44 100644 --- a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetry.java +++ b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetry.java @@ -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 extends Retry> { private final Supplier delayBetweenRetries; public CompletionStageRetry(FaultToleranceStrategy> delegate, String description, - ExceptionDecision exceptionDecision, long maxRetries, long maxTotalDurationInMillis, - Supplier delayBetweenRetries, Stopwatch stopwatch) { + ResultDecision resultDecision, ExceptionDecision exceptionDecision, long maxRetries, + long maxTotalDurationInMillis, Supplier 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"); } @@ -87,10 +89,14 @@ private CompletionStage afterDelay(InvocationContext> 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 { @@ -101,7 +107,7 @@ private CompletionStage afterDelay(InvocationContext> ctx, return result; } catch (Throwable e) { - if (shouldAbortRetrying(e)) { + if (shouldAbortRetryingOnException(e)) { ctx.fireEvent(RetryEvents.Finished.EXCEPTION_NOT_RETRYABLE); return failedFuture(e); } else { diff --git a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/Retry.java b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/Retry.java index a3d743bb7..711d05d4d 100644 --- a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/Retry.java +++ b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/retry/Retry.java @@ -13,21 +13,25 @@ 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 Retry implements FaultToleranceStrategy { final FaultToleranceStrategy delegate; final String description; + private final ResultDecision resultDecision; private final ExceptionDecision exceptionDecision; final long maxRetries; // this is an `int` in MP FT, but `long` allows easier handling of "infinity" final long maxTotalDurationInMillis; private final Supplier delayBetweenRetries; final Stopwatch stopwatch; - public Retry(FaultToleranceStrategy delegate, String description, ExceptionDecision exceptionDecision, - long maxRetries, long maxTotalDurationInMillis, Supplier delayBetweenRetries, Stopwatch stopwatch) { + public Retry(FaultToleranceStrategy delegate, String description, ResultDecision resultDecision, + ExceptionDecision exceptionDecision, long maxRetries, long maxTotalDurationInMillis, + Supplier delayBetweenRetries, Stopwatch stopwatch) { this.delegate = checkNotNull(delegate, "Retry delegate must be set"); this.description = checkNotNull(description, "Retry description must be set"); + this.resultDecision = checkNotNull(resultDecision, "Result decision must be set"); this.exceptionDecision = checkNotNull(exceptionDecision, "Exception decision must be set"); this.maxRetries = maxRetries < 0 ? Long.MAX_VALUE : maxRetries; this.maxTotalDurationInMillis = maxTotalDurationInMillis <= 0 ? Long.MAX_VALUE : maxTotalDurationInMillis; @@ -86,9 +90,13 @@ private V doApply(InvocationContext ctx) throws Exception { try { V result = delegate.apply(ctx); - ctx.fireEvent(RetryEvents.Finished.VALUE_RETURNED); - return result; + if (shouldAbortRetryingOnResult(result)) { + ctx.fireEvent(RetryEvents.Finished.VALUE_RETURNED); + return result; + } + lastFailure = null; } catch (InterruptedException e) { + ctx.fireEvent(RetryEvents.Finished.EXCEPTION_NOT_RETRYABLE); throw e; } catch (Throwable e) { if (Thread.interrupted()) { @@ -96,7 +104,7 @@ private V doApply(InvocationContext ctx) throws Exception { throw new InterruptedException(); } - if (shouldAbortRetrying(e)) { + if (shouldAbortRetryingOnException(e)) { ctx.fireEvent(RetryEvents.Finished.EXCEPTION_NOT_RETRYABLE); throw e; } @@ -116,12 +124,15 @@ private V doApply(InvocationContext ctx) throws Exception { if (lastFailure != null) { throw sneakyThrow(lastFailure); } else { - // this branch should never be taken throw new FaultToleranceException(description + " reached max retries or max retry duration"); } } - boolean shouldAbortRetrying(Throwable e) { + boolean shouldAbortRetryingOnResult(Object value) { + return resultDecision.isConsideredExpected(value); + } + + boolean shouldAbortRetryingOnException(Throwable e) { return exceptionDecision.isConsideredExpected(e); } } diff --git a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/PredicateBasedExceptionDecision.java b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/PredicateBasedExceptionDecision.java index e77b9ab24..e719b336d 100644 --- a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/PredicateBasedExceptionDecision.java +++ b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/PredicateBasedExceptionDecision.java @@ -1,12 +1,14 @@ package io.smallrye.faulttolerance.core.util; +import static io.smallrye.faulttolerance.core.util.Preconditions.checkNotNull; + import java.util.function.Predicate; public class PredicateBasedExceptionDecision implements ExceptionDecision { private final Predicate isExpected; public PredicateBasedExceptionDecision(Predicate isExpected) { - this.isExpected = isExpected; + this.isExpected = checkNotNull(isExpected, "Exception predicate must be set"); } @Override diff --git a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/PredicateBasedResultDecision.java b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/PredicateBasedResultDecision.java new file mode 100644 index 000000000..e130c2483 --- /dev/null +++ b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/PredicateBasedResultDecision.java @@ -0,0 +1,18 @@ +package io.smallrye.faulttolerance.core.util; + +import static io.smallrye.faulttolerance.core.util.Preconditions.checkNotNull; + +import java.util.function.Predicate; + +public class PredicateBasedResultDecision implements ResultDecision { + private final Predicate isExpected; + + public PredicateBasedResultDecision(Predicate isExpected) { + this.isExpected = checkNotNull(isExpected, "Result predicate must be set"); + } + + @Override + public boolean isConsideredExpected(Object obj) { + return isExpected.test(obj); + } +} diff --git a/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/ResultDecision.java b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/ResultDecision.java new file mode 100644 index 000000000..a207c8976 --- /dev/null +++ b/implementation/core/src/main/java/io/smallrye/faulttolerance/core/util/ResultDecision.java @@ -0,0 +1,8 @@ +package io.smallrye.faulttolerance.core.util; + +public interface ResultDecision { + boolean isConsideredExpected(Object obj); + + ResultDecision ALWAYS_EXPECTED = ignored -> true; + ResultDecision ALWAYS_FAILURE = ignored -> false; +} diff --git a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/composition/Strategies.java b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/composition/Strategies.java index 98138b0c6..2c3fbed10 100644 --- a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/composition/Strategies.java +++ b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/composition/Strategies.java @@ -8,6 +8,7 @@ import io.smallrye.faulttolerance.core.stopwatch.TestStopwatch; import io.smallrye.faulttolerance.core.timer.TestTimer; import io.smallrye.faulttolerance.core.util.ExceptionDecision; +import io.smallrye.faulttolerance.core.util.ResultDecision; /** * Factory methods for fault tolerance strategies that are easier to use than their constructors. @@ -21,7 +22,7 @@ static Fallback fallback(FaultToleranceStrategy delegate) { } static Retry retry(FaultToleranceStrategy delegate) { - return new Retry<>(delegate, "retry", ExceptionDecision.ALWAYS_FAILURE, + return new Retry<>(delegate, "retry", ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_FAILURE, 10, 0, SyncDelay.NONE, new TestStopwatch()); } diff --git a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetryTest.java b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetryTest.java index 18fee1202..f85cf4d05 100644 --- a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetryTest.java +++ b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/CompletionStageRetryTest.java @@ -12,6 +12,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,6 +21,7 @@ import io.smallrye.faulttolerance.core.async.CompletionStageExecution; import io.smallrye.faulttolerance.core.stopwatch.TestStopwatch; import io.smallrye.faulttolerance.core.util.ExceptionDecision; +import io.smallrye.faulttolerance.core.util.ResultDecision; import io.smallrye.faulttolerance.core.util.SetBasedExceptionDecision; import io.smallrye.faulttolerance.core.util.SetOfThrowables; @@ -47,13 +49,33 @@ public void shouldNotRetryOnSuccess() throws Exception { }); CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldNotRetryOnSuccess", - ExceptionDecision.ALWAYS_FAILURE, 3, 1000L, AsyncDelay.NONE, new TestStopwatch()); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_FAILURE, 3, 1000L, AsyncDelay.NONE, + new TestStopwatch()); CompletionStage result = retry.apply(new InvocationContext<>(() -> completedFuture("ignored"))); assertThat(result.toCompletableFuture().get()).isEqualTo("shouldNotRetryOnSuccess"); assertThat(invocationCount).hasValue(1); } + @Test + public void shouldRetryOnSuccessThatMatches() throws Exception { + AtomicInteger invocationCount = new AtomicInteger(0); + TestInvocation> invocation = TestInvocation.immediatelyReturning(() -> { + invocationCount.incrementAndGet(); + return CompletableFuture.supplyAsync(() -> "shouldRetryOnSuccessThatMatches"); + }); + CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); + CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldNotRetryOnSuccess", + ResultDecision.ALWAYS_FAILURE, ExceptionDecision.ALWAYS_FAILURE, 3, 1000L, AsyncDelay.NONE, + new TestStopwatch()); + + CompletionStage result = retry.apply(new InvocationContext<>(() -> completedFuture("ignored"))); + assertThatThrownBy(result.toCompletableFuture()::get) + .isExactlyInstanceOf(ExecutionException.class) + .hasCauseExactlyInstanceOf(FaultToleranceException.class); + assertThat(invocationCount).hasValue(4); + } + @Test public void shouldPropagateAbortOnError() { RuntimeException error = new RuntimeException("forced"); @@ -68,6 +90,7 @@ public void shouldPropagateAbortOnError() { }); CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldPropagateAbortOnError", + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(SetOfThrowables.ALL, SetOfThrowables.create(RuntimeException.class), false), 3, 1000L, AsyncDelay.NONE, new TestStopwatch()); @@ -89,6 +112,7 @@ public void shouldPropagateAbortOnErrorInCSCreation() { }); CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldPropagateAbortOnErrorInCSCreation", + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(SetOfThrowables.ALL, SetOfThrowables.create(RuntimeException.class), false), 3, 1000L, AsyncDelay.NONE, new TestStopwatch()); @@ -114,6 +138,7 @@ public void shouldRetryOnce() throws Exception { }); CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldRetryOnce", + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(SetOfThrowables.create(RuntimeException.class), SetOfThrowables.EMPTY, false), 3, 1000L, AsyncDelay.NONE, new TestStopwatch()); @@ -140,6 +165,7 @@ public void shouldRetryOnceOnCsFailure() throws Exception { }); CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldRetryOnceOnCsFailure", + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(SetOfThrowables.create(RuntimeException.class), SetOfThrowables.EMPTY, false), 3, 1000L, AsyncDelay.NONE, new TestStopwatch()); @@ -166,6 +192,7 @@ public void shouldRetryMaxTimesAndSucceed() throws Exception { }); CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldRetryMaxTimesAndSucceed", + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(SetOfThrowables.create(RuntimeException.class), SetOfThrowables.EMPTY, false), 3, 1000L, AsyncDelay.NONE, new TestStopwatch()); @@ -186,6 +213,7 @@ public void shouldRetryMaxTimesAndFail() { })); CompletionStageExecution execution = new CompletionStageExecution<>(invocation, executor); CompletionStageRetry retry = new CompletionStageRetry<>(execution, "shouldRetryMaxTimesAndSucceed", + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(SetOfThrowables.create(RuntimeException.class), SetOfThrowables.EMPTY, false), 3, 1000L, AsyncDelay.NONE, new TestStopwatch()); diff --git a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/FutureRetryTest.java b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/FutureRetryTest.java index 1fe533bdb..a34a69934 100644 --- a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/FutureRetryTest.java +++ b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/FutureRetryTest.java @@ -8,11 +8,13 @@ import java.util.concurrent.Future; +import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.smallrye.faulttolerance.core.stopwatch.TestStopwatch; import io.smallrye.faulttolerance.core.util.ExceptionDecision; +import io.smallrye.faulttolerance.core.util.ResultDecision; import io.smallrye.faulttolerance.core.util.SetBasedExceptionDecision; import io.smallrye.faulttolerance.core.util.SetOfThrowables; import io.smallrye.faulttolerance.core.util.TestException; @@ -38,7 +40,7 @@ public void immediatelyReturning_failedFuture() throws Exception { RuntimeException exception = new RuntimeException(); TestInvocation> invocation = TestInvocation.immediatelyReturning(() -> failedFuture(exception)); Retry> futureRetry = new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch); Future result = runOnTestThread(futureRetry).await(); assertThatThrownBy(result::get).hasCause(exception); assertThat(invocation.numberOfInvocations()).isEqualTo(1); @@ -48,12 +50,22 @@ public void immediatelyReturning_failedFuture() throws Exception { public void immediatelyReturning_value() throws Exception { TestInvocation> invocation = TestInvocation.immediatelyReturning(() -> completedFuture("foobar")); Retry> futureRetry = new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch); Future result = runOnTestThread(futureRetry).await(); assertThat(result.get()).isEqualTo("foobar"); assertThat(invocation.numberOfInvocations()).isEqualTo(1); } + @Test + public void immediatelyReturning_retriedValue() throws Exception { + TestInvocation> invocation = TestInvocation.immediatelyReturning(() -> completedFuture("foobar")); + Retry> futureRetry = new Retry<>(invocation, "test invocation", + ResultDecision.ALWAYS_FAILURE, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch); + TestThread> executingThread = runOnTestThread(futureRetry); + assertThatThrownBy(executingThread::await).isExactlyInstanceOf(FaultToleranceException.class); + assertThat(invocation.numberOfInvocations()).isEqualTo(4); + } + @Test public void immediatelyReturning_interruptedInInvocation() throws InterruptedException { Barrier startInvocationBarrier = Barrier.interruptible(); @@ -64,7 +76,7 @@ public void immediatelyReturning_interruptedInInvocation() throws InterruptedExc return completedFuture("foobar"); }); TestThread> executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); executingThread.interrupt(); assertThatThrownBy(executingThread::await).isInstanceOf(InterruptedException.class); @@ -82,7 +94,7 @@ public void immediatelyReturning_selfInterruptedInInvocation() throws Interrupte throw new RuntimeException(); }); TestThread> executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); endInvocationBarrier.open(); assertThatThrownBy(executingThread::await).isInstanceOf(InterruptedException.class); @@ -94,7 +106,7 @@ public void initiallyFailing_retriedExceptionThenValue_equalToMaxRetries() throw TestInvocation> invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> completedFuture("foobar")); TestThread> result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThat(result.await().get()).isEqualTo("foobar"); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -105,7 +117,7 @@ public void initiallyFailing_retriedExceptionThenValue_moreThanMaxRetries() { TestInvocation> invocation = TestInvocation.initiallyFailing(4, RuntimeException::new, () -> completedFuture("foobar")); TestThread> result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(RuntimeException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -116,7 +128,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_equalToMaxRetr TestInvocation> invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread> result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -132,7 +144,7 @@ public void initiallyFailing_retriedExceptionThenValue_interruptedInInvocation() return completedFuture("foobar"); }); TestThread> executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); executingThread.interrupt(); diff --git a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/RetryTest.java b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/RetryTest.java index 8a1342cc4..258c302f4 100644 --- a/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/RetryTest.java +++ b/implementation/core/src/test/java/io/smallrye/faulttolerance/core/retry/RetryTest.java @@ -4,11 +4,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.smallrye.faulttolerance.core.stopwatch.TestStopwatch; import io.smallrye.faulttolerance.core.util.ExceptionDecision; +import io.smallrye.faulttolerance.core.util.ResultDecision; import io.smallrye.faulttolerance.core.util.SetBasedExceptionDecision; import io.smallrye.faulttolerance.core.util.SetOfThrowables; import io.smallrye.faulttolerance.core.util.TestException; @@ -30,16 +32,27 @@ public void setUp() { public void immediatelyReturning_value() throws Exception { TestInvocation invocation = TestInvocation.immediatelyReturning(() -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, + SyncDelay.NONE, stopwatch)); assertThat(result.await()).isEqualTo("foobar"); assertThat(invocation.numberOfInvocations()).isEqualTo(1); } + @Test + public void immediatelyReturning_retriedValue() { + TestInvocation invocation = TestInvocation.immediatelyReturning(() -> "foobar"); + TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", + ResultDecision.ALWAYS_FAILURE, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, + SyncDelay.NONE, stopwatch)); + assertThatThrownBy(result::await).isExactlyInstanceOf(FaultToleranceException.class); + assertThat(invocation.numberOfInvocations()).isEqualTo(4); + } + @Test public void immediatelyReturning_retriedException() { TestInvocation invocation = TestInvocation.immediatelyReturning(TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -49,7 +62,7 @@ public void immediatelyReturning_retriedException() { public void immediatelyReturning_abortingException() { TestInvocation invocation = TestInvocation.immediatelyReturning(TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(1); @@ -59,7 +72,7 @@ public void immediatelyReturning_abortingException() { public void immediatelyReturning_unknownException() { TestInvocation invocation = TestInvocation.immediatelyReturning(TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(1); } @@ -74,7 +87,7 @@ public void immediatelyReturning_interruptedInInvocation() throws InterruptedExc return "foobar"; }); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); executingThread.interrupt(); assertThatThrownBy(executingThread::await).isInstanceOf(InterruptedException.class); @@ -92,7 +105,7 @@ public void immediatelyReturning_selfInterruptedInInvocation() throws Interrupte throw new RuntimeException(); }); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); + ResultDecision.ALWAYS_EXPECTED, ExceptionDecision.ALWAYS_EXPECTED, 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); endInvocationBarrier.open(); assertThatThrownBy(executingThread::await).isInstanceOf(InterruptedException.class); @@ -103,7 +116,7 @@ public void immediatelyReturning_selfInterruptedInInvocation() throws Interrupte public void initiallyFailing_retriedExceptionThenValue_lessThanMaxRetries() throws Exception { TestInvocation invocation = TestInvocation.initiallyFailing(2, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThat(result.await()).isEqualTo("foobar"); assertThat(invocation.numberOfInvocations()).isEqualTo(3); @@ -113,7 +126,7 @@ public void initiallyFailing_retriedExceptionThenValue_lessThanMaxRetries() thro public void initiallyFailing_retriedExceptionThenValue_equalToMaxRetries() throws Exception { TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThat(result.await()).isEqualTo("foobar"); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -123,7 +136,7 @@ public void initiallyFailing_retriedExceptionThenValue_equalToMaxRetries() throw public void initiallyFailing_retriedExceptionThenValue_moreThanMaxRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(4, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(RuntimeException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -133,7 +146,7 @@ public void initiallyFailing_retriedExceptionThenValue_moreThanMaxRetries() { public void initiallyFailing_retriedExceptionThenRetriedException_lessThanMaxRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(2, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -143,7 +156,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_lessThanMaxRet public void initiallyFailing_retriedExceptionThenRetriedException_equalToMaxRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -153,7 +166,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_equalToMaxRetr public void initiallyFailing_retriedExceptionThenRetriedException_moreThanMaxRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(4, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(RuntimeException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -163,7 +176,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_moreThanMaxRet public void initiallyFailing_retriedExceptionThenAbortingException_lessThanMaxRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(2, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(3); @@ -173,7 +186,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_lessThanMaxRe public void initiallyFailing_retriedExceptionThenAbortingException_equalToMaxRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -183,7 +196,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_equalToMaxRet public void initiallyFailing_retriedExceptionThenAbortingException_moreThanMaxRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(4, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(RuntimeException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(4); @@ -196,7 +209,7 @@ public void initiallyFailing_retriedExceptionThenValue_totalDelayLessThanMaxDura TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(500); @@ -212,7 +225,7 @@ public void initiallyFailing_retriedExceptionThenValue_totalDelayEqualToMaxDurat TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1000); @@ -228,7 +241,7 @@ public void initiallyFailing_retriedExceptionThenValue_totalDelayMoreThanMaxDura TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1500); @@ -245,7 +258,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_totalDelayLess TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(500); @@ -262,7 +275,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_totalDelayEqua TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1000); @@ -279,7 +292,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_totalDelayMore TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1500); @@ -296,7 +309,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_totalDelayLes TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(500); @@ -313,7 +326,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_totalDelayEqu TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1000); @@ -330,7 +343,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_totalDelayMor TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1500); @@ -343,7 +356,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_totalDelayMor public void initiallyFailing_retriedExceptionThenValue_infiniteRetries() throws Exception { TestInvocation invocation = TestInvocation.initiallyFailing(10, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), -1, 1000, SyncDelay.NONE, stopwatch)); assertThat(result.await()).isEqualTo("foobar"); assertThat(invocation.numberOfInvocations()).isEqualTo(11); @@ -353,7 +366,7 @@ public void initiallyFailing_retriedExceptionThenValue_infiniteRetries() throws public void initiallyFailing_retriedExceptionThenAbortingException_infiniteRetries() { TestInvocation invocation = TestInvocation.initiallyFailing(10, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), -1, 1000, SyncDelay.NONE, stopwatch)); assertThatThrownBy(result::await).isExactlyInstanceOf(TestException.class); assertThat(invocation.numberOfInvocations()).isEqualTo(11); @@ -366,7 +379,7 @@ public void initiallyFailing_retriedExceptionThenValue_infiniteDuration() throws TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, -1, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1_000_000_000L); @@ -382,7 +395,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_infiniteDurati TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, -1, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1_000_000_000L); @@ -398,7 +411,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_infiniteDurat TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread result = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, -1, () -> delay, stopwatch)); startDelayBarrier.await(); stopwatch.setCurrentValue(1_000_000_000L); @@ -417,7 +430,7 @@ public void initiallyFailing_retriedExceptionThenValue_interruptedInInvocation() return "foobar"; }); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); executingThread.interrupt(); @@ -435,7 +448,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_interruptedInI throw new TestException(); }); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); executingThread.interrupt(); @@ -453,7 +466,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_interruptedIn throw new TestException(); }); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); executingThread.interrupt(); @@ -468,7 +481,7 @@ public void initiallyFailing_retriedExceptionThenValue_interruptedInDelay() thro TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); executingThread.interrupt(); @@ -483,7 +496,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_interruptedInD TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); executingThread.interrupt(); @@ -498,7 +511,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_interruptedIn TestDelay delay = TestDelay.normal(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); executingThread.interrupt(); @@ -513,7 +526,7 @@ public void initiallyFailing_retriedExceptionThenValue_unexpectedExceptionInDela TestDelay delay = TestDelay.exceptionThrowing(startDelayBarrier, endDelayBarrier, RuntimeException::new); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); endDelayBarrier.open(); @@ -528,7 +541,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_unexpectedExce TestDelay delay = TestDelay.exceptionThrowing(startDelayBarrier, endDelayBarrier, RuntimeException::new); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); endDelayBarrier.open(); @@ -544,7 +557,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_unexpectedExc TestDelay delay = TestDelay.exceptionThrowing(startDelayBarrier, endDelayBarrier, RuntimeException::new); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); endDelayBarrier.open(); @@ -563,7 +576,7 @@ public void initiallyFailing_retriedExceptionThenSelfInterrupt() throws Interrup throw new RuntimeException(); }); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, SyncDelay.NONE, stopwatch)); startInvocationBarrier.await(); endInvocationBarrier.open(); @@ -578,7 +591,7 @@ public void initiallyFailing_retriedExceptionThenValue_selfInterruptedInDelay() TestDelay delay = TestDelay.selfInterrupting(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, () -> "foobar"); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); endDelayBarrier.open(); @@ -593,7 +606,7 @@ public void initiallyFailing_retriedExceptionThenRetriedException_selfInterrupte TestDelay delay = TestDelay.selfInterrupting(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, SetOfThrowables.EMPTY, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); endDelayBarrier.open(); @@ -608,7 +621,7 @@ public void initiallyFailing_retriedExceptionThenAbortingException_selfInterrupt TestDelay delay = TestDelay.selfInterrupting(startDelayBarrier, endDelayBarrier); TestInvocation invocation = TestInvocation.initiallyFailing(3, RuntimeException::new, TestException::doThrow); TestThread executingThread = runOnTestThread(new Retry<>(invocation, "test invocation", - new SetBasedExceptionDecision(exception, testException, false), + ResultDecision.ALWAYS_EXPECTED, new SetBasedExceptionDecision(exception, testException, false), 3, 1000, () -> delay, stopwatch)); startDelayBarrier.await(); endDelayBarrier.open(); diff --git a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/CdiLogger.java b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/CdiLogger.java index fa0118b64..36ef31b53 100644 --- a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/CdiLogger.java +++ b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/CdiLogger.java @@ -37,4 +37,9 @@ interface CdiLogger extends BasicLogger { DefinitionException bothAsyncAndAsyncNonBlockingPresent(MethodDescriptor method); DefinitionException bothAsyncAndAsyncNonBlockingPresent(Class clazz); + + @Message(id = 6, value = "@RetryWhen present on '%s', but @Retry is missing") + DefinitionException retryWhenAnnotationWithoutRetry(MethodDescriptor method); + + DefinitionException retryWhenAnnotationWithoutRetry(Class clazz); } diff --git a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceExtension.java b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceExtension.java index 4c3809867..3eddcfc4f 100644 --- a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceExtension.java +++ b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceExtension.java @@ -64,6 +64,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; import io.smallrye.faulttolerance.autoconfig.FaultToleranceMethod; import io.smallrye.faulttolerance.config.FaultToleranceMethods; import io.smallrye.faulttolerance.config.FaultToleranceOperation; @@ -198,6 +199,16 @@ void collectFaultToleranceOperations(@Observes ProcessManagedBean event) { } } + if (annotatedMethod.isAnnotationPresent(RetryWhen.class) + && !annotatedMethod.isAnnotationPresent(Retry.class)) { + event.addDefinitionError(LOG.retryWhenAnnotationWithoutRetry(method.method)); + } + + if (annotatedType.isAnnotationPresent(RetryWhen.class) + && !annotatedType.isAnnotationPresent(Retry.class)) { + event.addDefinitionError(LOG.retryWhenAnnotationWithoutRetry(annotatedType.getJavaClass())); + } + if (annotatedMethod.isAnnotationPresent(Asynchronous.class) && annotatedMethod.isAnnotationPresent(AsynchronousNonBlocking.class)) { event.addDefinitionError(LOG.bothAsyncAndAsyncNonBlockingPresent(method.method)); diff --git a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceInterceptor.java b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceInterceptor.java index 93b9dc1cf..3e57dba7d 100644 --- a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceInterceptor.java +++ b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/FaultToleranceInterceptor.java @@ -29,6 +29,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.function.Predicate; import java.util.function.Supplier; import jakarta.annotation.Priority; @@ -46,8 +47,10 @@ import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceException; import io.smallrye.common.annotation.Identifier; +import io.smallrye.faulttolerance.api.AlwaysOnException; import io.smallrye.faulttolerance.api.CustomBackoffStrategy; import io.smallrye.faulttolerance.api.FaultTolerance; +import io.smallrye.faulttolerance.api.NeverOnResult; import io.smallrye.faulttolerance.config.FaultToleranceOperation; import io.smallrye.faulttolerance.core.FaultToleranceStrategy; import io.smallrye.faulttolerance.core.apiimpl.LazyFaultTolerance; @@ -93,6 +96,9 @@ import io.smallrye.faulttolerance.core.timer.Timer; import io.smallrye.faulttolerance.core.util.DirectExecutor; import io.smallrye.faulttolerance.core.util.ExceptionDecision; +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; import io.smallrye.faulttolerance.internal.FallbackMethod; @@ -332,7 +338,9 @@ private FaultToleranceStrategy> prepareAsyncStrategy(Faul Supplier backoff = prepareRetryBackoff(operation); result = new CompletionStageRetry<>(result, point.toString(), - createExceptionDecision(operation.getRetry().abortOn(), operation.getRetry().retryOn()), + createResultDecision(operation.hasRetryWhen() ? operation.getRetryWhen().result() : null), + createExceptionDecision(operation.getRetry().abortOn(), operation.getRetry().retryOn(), + operation.hasRetryWhen() ? operation.getRetryWhen().exception() : null), operation.getRetry().maxRetries(), maxDurationMs, () -> new TimerDelay(backoff.get(), timer), @@ -402,7 +410,9 @@ private FaultToleranceStrategy prepareSyncStrategy(FaultToleranceOperatio Supplier backoff = prepareRetryBackoff(operation); result = new Retry<>(result, point.toString(), - createExceptionDecision(operation.getRetry().abortOn(), operation.getRetry().retryOn()), + createResultDecision(operation.hasRetryWhen() ? operation.getRetryWhen().result() : null), + createExceptionDecision(operation.getRetry().abortOn(), operation.getRetry().retryOn(), + operation.hasRetryWhen() ? operation.getRetryWhen().exception() : null), operation.getRetry().maxRetries(), maxDurationMs, () -> new ThreadSleepDelay(backoff.get()), @@ -473,7 +483,9 @@ private FaultToleranceStrategy> prepareFutureStrategy(FaultToleran Supplier backoff = prepareRetryBackoff(operation); result = new Retry<>(result, point.toString(), - createExceptionDecision(operation.getRetry().abortOn(), operation.getRetry().retryOn()), + createResultDecision(operation.hasRetryWhen() ? operation.getRetryWhen().result() : null), + createExceptionDecision(operation.getRetry().abortOn(), operation.getRetry().retryOn(), + operation.hasRetryWhen() ? operation.getRetryWhen().exception() : null), operation.getRetry().maxRetries(), maxDurationMs, () -> new ThreadSleepDelay(backoff.get()), @@ -516,8 +528,8 @@ private Supplier prepareRetryBackoff(FaultToleranceOperation operation) return () -> { CustomBackoffStrategy instance; try { - instance = strategy.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { + instance = strategy.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { throw sneakyThrow(e); } instance.init(delayMs); @@ -578,12 +590,40 @@ private FallbackFunction prepareFallbackFunction(InterceptionPoint point, return fallbackFunction; } + private ResultDecision createResultDecision(Class> whenResult) { + if (whenResult != null && whenResult != NeverOnResult.class) { + Predicate predicate; + try { + predicate = whenResult.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw sneakyThrow(e); + } + return new PredicateBasedResultDecision(predicate.negate()); + } + return ResultDecision.ALWAYS_EXPECTED; + } + private ExceptionDecision createExceptionDecision(Class[] consideredExpected, Class[] consideredFailure) { return new SetBasedExceptionDecision(createSetOfThrowables(consideredFailure), createSetOfThrowables(consideredExpected), specCompatibility.inspectExceptionCauseChain()); } + private ExceptionDecision createExceptionDecision(Class[] consideredExpected, + Class[] consideredFailure, Class> whenException) { + if (whenException != null && whenException != AlwaysOnException.class) { + Predicate predicate; + try { + predicate = whenException.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw sneakyThrow(e); + } + return new PredicateBasedExceptionDecision(predicate.negate()); + } + return new SetBasedExceptionDecision(createSetOfThrowables(consideredFailure), + createSetOfThrowables(consideredExpected), specCompatibility.inspectExceptionCauseChain()); + } + private SetOfThrowables createSetOfThrowables(Class[] throwableClasses) { if (throwableClasses == null) { return SetOfThrowables.EMPTY; diff --git a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceMethods.java b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceMethods.java index e717fdf82..2d8fb7a6a 100644 --- a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceMethods.java +++ b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceMethods.java @@ -23,6 +23,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; import io.smallrye.faulttolerance.autoconfig.FaultToleranceMethod; import io.smallrye.faulttolerance.autoconfig.MethodDescriptor; @@ -53,6 +54,7 @@ public static FaultToleranceMethod create(AnnotatedMethod method) { result.customBackoff = getAnnotation(CustomBackoff.class, method, annotationsPresentDirectly); result.exponentialBackoff = getAnnotation(ExponentialBackoff.class, method, annotationsPresentDirectly); result.fibonacciBackoff = getAnnotation(FibonacciBackoff.class, method, annotationsPresentDirectly); + result.retryWhen = getAnnotation(RetryWhen.class, method, annotationsPresentDirectly); result.annotationsPresentDirectly = annotationsPresentDirectly; @@ -104,6 +106,7 @@ public static FaultToleranceMethod create(Class beanClass, Method method) { result.customBackoff = getAnnotation(CustomBackoff.class, method, beanClass, annotationsPresentDirectly); result.exponentialBackoff = getAnnotation(ExponentialBackoff.class, method, beanClass, annotationsPresentDirectly); result.fibonacciBackoff = getAnnotation(FibonacciBackoff.class, method, beanClass, annotationsPresentDirectly); + result.retryWhen = getAnnotation(RetryWhen.class, method, beanClass, annotationsPresentDirectly); result.annotationsPresentDirectly = annotationsPresentDirectly; diff --git a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceOperation.java b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceOperation.java index 706f773a8..183c60f25 100644 --- a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceOperation.java +++ b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/FaultToleranceOperation.java @@ -32,6 +32,7 @@ import io.smallrye.common.annotation.Blocking; import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.faulttolerance.api.AlwaysOnException; import io.smallrye.faulttolerance.api.ApplyFaultTolerance; import io.smallrye.faulttolerance.api.AsynchronousNonBlocking; import io.smallrye.faulttolerance.api.CircuitBreakerName; @@ -39,6 +40,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; import io.smallrye.faulttolerance.autoconfig.Config; import io.smallrye.faulttolerance.autoconfig.FaultToleranceMethod; import io.smallrye.faulttolerance.autoconfig.MethodDescriptor; @@ -64,7 +66,8 @@ public static FaultToleranceOperation create(FaultToleranceMethod method) { TimeoutConfigImpl.create(method), ExponentialBackoffConfigImpl.create(method), FibonacciBackoffConfigImpl.create(method), - CustomBackoffConfigImpl.create(method)); + CustomBackoffConfigImpl.create(method), + RetryWhenConfigImpl.create(method)); } private final Class beanClass; @@ -88,6 +91,7 @@ public static FaultToleranceOperation create(FaultToleranceMethod method) { private final ExponentialBackoffConfig exponentialBackoff; private final FibonacciBackoffConfig fibonacciBackoff; private final CustomBackoffConfig customBackoff; + private final RetryWhenConfig retryWhen; private FaultToleranceOperation(Class beanClass, MethodDescriptor methodDescriptor, @@ -105,7 +109,8 @@ private FaultToleranceOperation(Class beanClass, TimeoutConfig timeout, ExponentialBackoffConfig exponentialBackoff, FibonacciBackoffConfig fibonacciBackoff, - CustomBackoffConfig customBackoff) { + CustomBackoffConfig customBackoff, + RetryWhenConfig retryWhen) { this.beanClass = beanClass; this.methodDescriptor = methodDescriptor; @@ -127,6 +132,7 @@ private FaultToleranceOperation(Class beanClass, this.exponentialBackoff = exponentialBackoff; this.fibonacciBackoff = fibonacciBackoff; this.customBackoff = customBackoff; + this.retryWhen = retryWhen; } public Class getBeanClass() { @@ -322,6 +328,14 @@ public CustomBackoff getCustomBackoff() { return customBackoff; } + public boolean hasRetryWhen() { + return retryWhen != null; + } + + public RetryWhen getRetryWhen() { + return retryWhen; + } + public String getName() { return beanClass.getCanonicalName() + "." + methodDescriptor.name; } @@ -376,6 +390,7 @@ public void validate() { } validateRetryBackoff(); + validateRetryWhen(); } private void validateRetryBackoff() { @@ -419,6 +434,29 @@ private void validateRetryBackoff() { } } + private void validateRetryWhen() { + if (retryWhen == null) { + return; + } + + retryWhen.validate(); + + if (retry == null) { + throw new FaultToleranceDefinitionException("Invalid @RetryWhen on " + methodDescriptor + ": missing @Retry"); + } + + if (retryWhen.exception() != AlwaysOnException.class) { + if (retry.abortOn().length != 0) { + throw new FaultToleranceDefinitionException("Invalid @RetryWhen.exception on " + methodDescriptor + + ": must not be combined with @Retry.abortOn"); + } + if (retry.retryOn().length != 1 || retry.retryOn()[0] != Exception.class) { + throw new FaultToleranceDefinitionException("Invalid @RetryWhen.exception on " + methodDescriptor + + ": must not be combined with @Retry.retryOn"); + } + } + } + /** * Ensures all configuration of this fault tolerance operation is loaded. Subsequent method invocations * on this instance are guaranteed to not touch MP Config. @@ -472,6 +510,9 @@ public void materialize() { if (customBackoff != null) { customBackoff.materialize(); } + if (retryWhen != null) { + retryWhen.materialize(); + } } @Override diff --git a/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/RetryWhenConfig.java b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/RetryWhenConfig.java new file mode 100644 index 000000000..e5c3504d1 --- /dev/null +++ b/implementation/fault-tolerance/src/main/java/io/smallrye/faulttolerance/config/RetryWhenConfig.java @@ -0,0 +1,12 @@ +package io.smallrye.faulttolerance.config; + +import io.smallrye.faulttolerance.api.RetryWhen; +import io.smallrye.faulttolerance.autoconfig.AutoConfig; +import io.smallrye.faulttolerance.autoconfig.Config; + +@AutoConfig +public interface RetryWhenConfig extends RetryWhen, Config { + @Override + default void validate() { + } +} diff --git a/implementation/mutiny/src/test/java/io/smallrye/faulttolerance/mutiny/test/MutinyRetryTest.java b/implementation/mutiny/src/test/java/io/smallrye/faulttolerance/mutiny/test/MutinyRetryTest.java index f30e2106b..d55480451 100644 --- a/implementation/mutiny/src/test/java/io/smallrye/faulttolerance/mutiny/test/MutinyRetryTest.java +++ b/implementation/mutiny/src/test/java/io/smallrye/faulttolerance/mutiny/test/MutinyRetryTest.java @@ -63,7 +63,7 @@ public void retryWithRetryOn() { @Test public void retryWithWhen() { Supplier> guarded = MutinyFaultTolerance.createSupplier(this::action) - .withRetry().maxRetries(3).when(e -> e instanceof RuntimeException).done() + .withRetry().maxRetries(3).whenException(e -> e instanceof RuntimeException).done() .withFallback().handler(this::fallback).when(e -> e instanceof TestException).done() .build(); diff --git a/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryAsyncTest.java b/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryAsyncTest.java index 57d00114e..e485dcead 100644 --- a/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryAsyncTest.java +++ b/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryAsyncTest.java @@ -4,6 +4,7 @@ import static java.util.concurrent.CompletableFuture.failedFuture; import static org.assertj.core.api.Assertions.assertThat; +import java.util.Objects; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -26,7 +27,7 @@ public void setUp() { @Test public void asyncRetry() { - Supplier> guarded = FaultTolerance.createAsyncSupplier(this::action) + Supplier> guarded = FaultTolerance.createAsyncSupplier(this::actionFail) .withRetry().maxRetries(3).done() .withFallback().handler(this::fallback).applyOn(TestException.class).done() .build(); @@ -39,7 +40,7 @@ public void asyncRetry() { @Test public void asyncRetryWithAbortOn() { - Supplier> guarded = FaultTolerance.createAsyncSupplier(this::action) + Supplier> guarded = FaultTolerance.createAsyncSupplier(this::actionFail) .withRetry().maxRetries(3).abortOn(TestException.class).done() .withFallback().handler(this::fallback).applyOn(TestException.class).done() .build(); @@ -52,7 +53,7 @@ public void asyncRetryWithAbortOn() { @Test public void asyncRetryWithRetryOn() { - Supplier> guarded = FaultTolerance.createAsyncSupplier(this::action) + Supplier> guarded = FaultTolerance.createAsyncSupplier(this::actionFail) .withRetry().maxRetries(3).retryOn(RuntimeException.class).done() .withFallback().handler(this::fallback).applyOn(TestException.class).done() .build(); @@ -64,9 +65,9 @@ public void asyncRetryWithRetryOn() { } @Test - public void asyncRetryWithWhen() { - Supplier> guarded = FaultTolerance.createAsyncSupplier(this::action) - .withRetry().maxRetries(3).when(e -> e instanceof RuntimeException).done() + public void asyncRetryWithWhenException() { + Supplier> guarded = FaultTolerance.createAsyncSupplier(this::actionFail) + .withRetry().maxRetries(3).whenException(e -> e instanceof RuntimeException).done() .withFallback().handler(this::fallback).when(e -> e instanceof TestException).done() .build(); @@ -76,11 +77,24 @@ public void asyncRetryWithWhen() { assertThat(counter).hasValue(1); // 1 initial invocation } + @Test + public void asyncRetryWithWhenResult() { + Supplier> guarded = FaultTolerance.createAsyncSupplier(this::actionReturnNull) + .withRetry().maxRetries(3).whenResult(Objects::isNull).done() + .withFallback().handler(this::fallback).done() + .build(); + + assertThat(guarded.get()) + .succeedsWithin(10, TimeUnit.SECONDS) + .isEqualTo("fallback"); + assertThat(counter).hasValue(4); // 1 initial invocation + 3 retries + } + @Test public void synchronousFlow() { // this is usually a mistake, because it only guards the synchronous execution // only testing it here to verify that indeed asynchronous fault tolerance doesn't apply - Supplier> guarded = FaultTolerance.createSupplier(this::action) + Supplier> guarded = FaultTolerance.createSupplier(this::actionFail) .withRetry().maxRetries(3).abortOn(TestException.class).done() .withFallback().handler(this::fallback).done() .build(); @@ -92,11 +106,16 @@ public void synchronousFlow() { assertThat(counter).hasValue(1); // 1 initial invocation } - public CompletionStage action() { + public CompletionStage actionFail() { counter.incrementAndGet(); return failedFuture(new TestException()); } + public CompletionStage actionReturnNull() { + counter.incrementAndGet(); + return completedFuture(null); + } + public CompletionStage fallback() { return completedFuture("fallback"); } diff --git a/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryTest.java b/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryTest.java index 47f0edbaa..da170aa29 100644 --- a/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryTest.java +++ b/implementation/standalone/src/test/java/io/smallrye/faulttolerance/standalone/test/StandaloneRetryTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Objects; import java.util.concurrent.Callable; import org.junit.jupiter.api.BeforeEach; @@ -20,7 +21,7 @@ public void setUp() { @Test public void retry() throws Exception { - Callable guarded = FaultTolerance.createCallable(this::action) + Callable guarded = FaultTolerance.createCallable(this::actionThrow) .withRetry().maxRetries(3).done() .withFallback().applyOn(TestException.class).handler(this::fallback).done() .build(); @@ -31,7 +32,7 @@ public void retry() throws Exception { @Test public void retryWithAbortOn() throws Exception { - Callable guarded = FaultTolerance.createCallable(this::action) + Callable guarded = FaultTolerance.createCallable(this::actionThrow) .withRetry().maxRetries(3).abortOn(TestException.class).done() .withFallback().applyOn(TestException.class).handler(this::fallback).done() .build(); @@ -42,7 +43,7 @@ public void retryWithAbortOn() throws Exception { @Test public void retryWithRetryOn() throws Exception { - Callable guarded = FaultTolerance.createCallable(this::action) + Callable guarded = FaultTolerance.createCallable(this::actionThrow) .withRetry().maxRetries(3).retryOn(RuntimeException.class).done() .withFallback().applyOn(TestException.class).handler(this::fallback).done() .build(); @@ -52,9 +53,9 @@ public void retryWithRetryOn() throws Exception { } @Test - public void retryWithWhen() throws Exception { - Callable guarded = FaultTolerance.createCallable(this::action) - .withRetry().maxRetries(3).when(e -> e instanceof RuntimeException).done() + public void retryWithWhenException() throws Exception { + Callable guarded = FaultTolerance.createCallable(this::actionThrow) + .withRetry().maxRetries(3).whenException(e -> e instanceof RuntimeException).done() .withFallback().when(e -> e instanceof TestException).handler(this::fallback).done() .build(); @@ -62,11 +63,27 @@ public void retryWithWhen() throws Exception { assertThat(counter).isEqualTo(1); // 1 initial invocation } - public String action() throws TestException { + @Test + public void retryWithWhenResult() throws Exception { + Callable guarded = FaultTolerance.createCallable(this::actionReturnNull) + .withRetry().maxRetries(3).whenResult(Objects::isNull).done() + .withFallback().handler(this::fallback).done() + .build(); + + assertThat(guarded.call()).isEqualTo("fallback"); + assertThat(counter).isEqualTo(4); // 1 initial invocation + 3 retries + } + + public String actionThrow() throws TestException { counter++; throw new TestException(); } + public String actionReturnNull() throws TestException { + counter++; + return null; + } + public String fallback() { return "fallback"; } diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/IsIllegalArgumentException.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/IsIllegalArgumentException.java new file mode 100644 index 000000000..58d13453e --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/IsIllegalArgumentException.java @@ -0,0 +1,10 @@ +package io.smallrye.faulttolerance.retry.when; + +import java.util.function.Predicate; + +public class IsIllegalArgumentException implements Predicate { + @Override + public boolean test(Throwable throwable) { + return throwable instanceof IllegalArgumentException; + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/IsNull.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/IsNull.java new file mode 100644 index 000000000..52ea64ebb --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/IsNull.java @@ -0,0 +1,10 @@ +package io.smallrye.faulttolerance.retry.when; + +import java.util.function.Predicate; + +public class IsNull implements Predicate { + @Override + public boolean test(Object o) { + return o == null; + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/both/RetryWhenResultAndExceptionService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/both/RetryWhenResultAndExceptionService.java new file mode 100644 index 000000000..2fed768c6 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/both/RetryWhenResultAndExceptionService.java @@ -0,0 +1,33 @@ +package io.smallrye.faulttolerance.retry.when.both; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; +import io.smallrye.faulttolerance.retry.when.IsIllegalArgumentException; +import io.smallrye.faulttolerance.retry.when.IsNull; + +@ApplicationScoped +public class RetryWhenResultAndExceptionService { + private final AtomicInteger attempts = new AtomicInteger(); + + @Retry + @RetryWhen(result = IsNull.class, exception = IsIllegalArgumentException.class) + public String hello() { + int current = attempts.incrementAndGet(); + if (current == 1) { + return null; + } else if (current == 2) { + throw new IllegalArgumentException(); + } else { + return "hello"; + } + } + + public AtomicInteger getAttempts() { + return attempts; + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/both/RetryWhenResultAndExceptionTest.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/both/RetryWhenResultAndExceptionTest.java new file mode 100644 index 000000000..f8da9d751 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/both/RetryWhenResultAndExceptionTest.java @@ -0,0 +1,16 @@ +package io.smallrye.faulttolerance.retry.when.both; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.smallrye.faulttolerance.util.FaultToleranceBasicTest; + +@FaultToleranceBasicTest +public class RetryWhenResultAndExceptionTest { + @Test + public void test(RetryWhenResultAndExceptionService service) { + assertThat(service.hello()).isEqualTo("hello"); + assertThat(service.getAttempts()).hasValue(3); + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnAndRetryWhenExceptionService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnAndRetryWhenExceptionService.java new file mode 100644 index 000000000..f8937bbad --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnAndRetryWhenExceptionService.java @@ -0,0 +1,16 @@ +package io.smallrye.faulttolerance.retry.when.error; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; +import io.smallrye.faulttolerance.retry.when.IsIllegalArgumentException; + +@ApplicationScoped +public class RetryOnAndRetryWhenExceptionService { + @Retry(retryOn = IllegalStateException.class) + @RetryWhen(exception = IsIllegalArgumentException.class) + public void hello() { + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnAndRetryWhenExceptionTest.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnAndRetryWhenExceptionTest.java new file mode 100644 index 000000000..7a6fdf289 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnAndRetryWhenExceptionTest.java @@ -0,0 +1,16 @@ +package io.smallrye.faulttolerance.retry.when.error; + +import jakarta.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Test; + +import io.smallrye.faulttolerance.util.ExpectedDeploymentException; +import io.smallrye.faulttolerance.util.FaultToleranceBasicTest; + +@FaultToleranceBasicTest +@ExpectedDeploymentException(DefinitionException.class) +public class RetryOnAndRetryWhenExceptionTest { + @Test + public void test(RetryOnAndRetryWhenExceptionService ignored) { + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnClassRetryWhenOnMethodService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnClassRetryWhenOnMethodService.java new file mode 100644 index 000000000..d2b84a683 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnClassRetryWhenOnMethodService.java @@ -0,0 +1,16 @@ +package io.smallrye.faulttolerance.retry.when.error; + +import jakarta.enterprise.context.Dependent; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; + +@Dependent +@Retry +public class RetryOnClassRetryWhenOnMethodService { + @RetryWhen + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnClassRetryWhenOnMethodTest.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnClassRetryWhenOnMethodTest.java new file mode 100644 index 000000000..16118e0b1 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnClassRetryWhenOnMethodTest.java @@ -0,0 +1,16 @@ +package io.smallrye.faulttolerance.retry.when.error; + +import jakarta.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Test; + +import io.smallrye.faulttolerance.util.ExpectedDeploymentException; +import io.smallrye.faulttolerance.util.FaultToleranceBasicTest; + +@FaultToleranceBasicTest +@ExpectedDeploymentException(DefinitionException.class) +public class RetryOnClassRetryWhenOnMethodTest { + @Test + public void test(RetryOnClassRetryWhenOnMethodService ignored) { + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnMethodRetryWhenOnClassService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnMethodRetryWhenOnClassService.java new file mode 100644 index 000000000..ef887c002 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnMethodRetryWhenOnClassService.java @@ -0,0 +1,16 @@ +package io.smallrye.faulttolerance.retry.when.error; + +import jakarta.enterprise.context.Dependent; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; + +@Dependent +@RetryWhen +public class RetryOnMethodRetryWhenOnClassService { + @Retry + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnMethodRetryWhenOnClassTest.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnMethodRetryWhenOnClassTest.java new file mode 100644 index 000000000..80f425401 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/error/RetryOnMethodRetryWhenOnClassTest.java @@ -0,0 +1,16 @@ +package io.smallrye.faulttolerance.retry.when.error; + +import jakarta.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Test; + +import io.smallrye.faulttolerance.util.ExpectedDeploymentException; +import io.smallrye.faulttolerance.util.FaultToleranceBasicTest; + +@FaultToleranceBasicTest +@ExpectedDeploymentException(DefinitionException.class) +public class RetryOnMethodRetryWhenOnClassTest { + @Test + public void test(RetryOnMethodRetryWhenOnClassService ignored) { + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionDoesNotMatchService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionDoesNotMatchService.java new file mode 100644 index 000000000..ac39a8b10 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionDoesNotMatchService.java @@ -0,0 +1,26 @@ +package io.smallrye.faulttolerance.retry.when.exception; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; +import io.smallrye.faulttolerance.retry.when.IsIllegalArgumentException; + +@ApplicationScoped +public class RetryWhenExceptionDoesNotMatchService { + private final AtomicInteger attempts = new AtomicInteger(); + + @Retry + @RetryWhen(exception = IsIllegalArgumentException.class) + public void hello() { + attempts.incrementAndGet(); + throw new IllegalStateException(); + } + + public AtomicInteger getAttempts() { + return attempts; + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionMatchesService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionMatchesService.java new file mode 100644 index 000000000..bd1153785 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionMatchesService.java @@ -0,0 +1,26 @@ +package io.smallrye.faulttolerance.retry.when.exception; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; +import io.smallrye.faulttolerance.retry.when.IsIllegalArgumentException; + +@ApplicationScoped +public class RetryWhenExceptionMatchesService { + private final AtomicInteger attempts = new AtomicInteger(); + + @Retry + @RetryWhen(exception = IsIllegalArgumentException.class) + public void hello() { + attempts.incrementAndGet(); + throw new IllegalArgumentException(); + } + + public AtomicInteger getAttempts() { + return attempts; + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionTest.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionTest.java new file mode 100644 index 000000000..4db1812b2 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/exception/RetryWhenExceptionTest.java @@ -0,0 +1,27 @@ +package io.smallrye.faulttolerance.retry.when.exception; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import io.smallrye.faulttolerance.util.FaultToleranceBasicTest; + +@FaultToleranceBasicTest +public class RetryWhenExceptionTest { + @Test + public void matchingException(RetryWhenExceptionMatchesService service) { + assertThatThrownBy(() -> { + service.hello(); + }).isExactlyInstanceOf(IllegalArgumentException.class); + assertThat(service.getAttempts()).hasValue(4); + } + + @Test + public void nonMatchingException(RetryWhenExceptionDoesNotMatchService service) { + assertThatThrownBy(() -> { + service.hello(); + }).isExactlyInstanceOf(IllegalStateException.class); + assertThat(service.getAttempts()).hasValue(1); + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultDoesNotMatchService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultDoesNotMatchService.java new file mode 100644 index 000000000..7abfa6de7 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultDoesNotMatchService.java @@ -0,0 +1,26 @@ +package io.smallrye.faulttolerance.retry.when.result; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; +import io.smallrye.faulttolerance.retry.when.IsNull; + +@ApplicationScoped +public class RetryWhenResultDoesNotMatchService { + private final AtomicInteger attempts = new AtomicInteger(); + + @Retry + @RetryWhen(result = IsNull.class) + public String hello() { + attempts.incrementAndGet(); + return "hello"; + } + + public AtomicInteger getAttempts() { + return attempts; + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultMatchesService.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultMatchesService.java new file mode 100644 index 000000000..552c64953 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultMatchesService.java @@ -0,0 +1,26 @@ +package io.smallrye.faulttolerance.retry.when.result; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.RetryWhen; +import io.smallrye.faulttolerance.retry.when.IsNull; + +@ApplicationScoped +public class RetryWhenResultMatchesService { + private final AtomicInteger attempts = new AtomicInteger(); + + @Retry + @RetryWhen(result = IsNull.class) + public String hello() { + attempts.incrementAndGet(); + return null; + } + + public AtomicInteger getAttempts() { + return attempts; + } +} diff --git a/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultTest.java b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultTest.java new file mode 100644 index 000000000..cdea1b127 --- /dev/null +++ b/testsuite/basic/src/test/java/io/smallrye/faulttolerance/retry/when/result/RetryWhenResultTest.java @@ -0,0 +1,26 @@ +package io.smallrye.faulttolerance.retry.when.result; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceException; +import org.junit.jupiter.api.Test; + +import io.smallrye.faulttolerance.util.FaultToleranceBasicTest; + +@FaultToleranceBasicTest +public class RetryWhenResultTest { + @Test + public void matchingResult(RetryWhenResultMatchesService service) { + assertThatThrownBy(() -> { + service.hello(); + }).isExactlyInstanceOf(FaultToleranceException.class); + assertThat(service.getAttempts()).hasValue(4); + } + + @Test + public void nonMatchingResult(RetryWhenResultDoesNotMatchService service) { + assertThat(service.hello()).isEqualTo("hello"); + assertThat(service.getAttempts()).hasValue(1); + } +}