diff --git a/src/main/java/org/springframework/retry/RetryContext.java b/src/main/java/org/springframework/retry/RetryContext.java index cec6ac55..0450ec5c 100644 --- a/src/main/java/org/springframework/retry/RetryContext.java +++ b/src/main/java/org/springframework/retry/RetryContext.java @@ -23,6 +23,7 @@ * used to alter the course of the retry, e.g. force an early termination. * * @author Dave Syer + * @author Emanuele Ivaldi * */ public interface RetryContext extends AttributeAccessor { @@ -61,6 +62,13 @@ public interface RetryContext extends AttributeAccessor { */ String NO_RECOVERY = "context.no-recovery"; + /** + * Retry context attribute that represent the maximum number of attempts for policies + * that provide a maximum number of attempts before failure. For other policies the + * value returned is {@link RetryPolicy#NO_MAXIMUM_ATTEMPTS_SET} + */ + String MAX_ATTEMPTS = "context.max-attempts"; + /** * Signal to the framework that no more attempts should be made to try or retry the * current {@link RetryCallback}. diff --git a/src/main/java/org/springframework/retry/RetryPolicy.java b/src/main/java/org/springframework/retry/RetryPolicy.java index f333014b..7185fd03 100644 --- a/src/main/java/org/springframework/retry/RetryPolicy.java +++ b/src/main/java/org/springframework/retry/RetryPolicy.java @@ -26,10 +26,17 @@ * API for a range of different platforms for the external context. * * @author Dave Syer + * @author Emanuele Ivaldi * */ public interface RetryPolicy extends Serializable { + /** + * The value returned by {@link RetryPolicy#getMaxAttempts()} when the policy doesn't + * provide a maximum number of attempts before failure + */ + int NO_MAXIMUM_ATTEMPTS_SET = -1; + /** * @param context the current retry status * @return true if the operation can proceed @@ -59,4 +66,14 @@ public interface RetryPolicy extends Serializable { */ void registerThrowable(RetryContext context, Throwable throwable); + /** + * Called to understand if the policy has a fixed number of maximum attempts before + * failure + * @return -1 if the policy doesn't provide a fixed number of maximum attempts before + * failure, the number of maximum attempts before failure otherwise + */ + default int getMaxAttempts() { + return NO_MAXIMUM_ATTEMPTS_SET; + } + } diff --git a/src/main/java/org/springframework/retry/policy/CompositeRetryPolicy.java b/src/main/java/org/springframework/retry/policy/CompositeRetryPolicy.java index 67961783..6a97d15d 100644 --- a/src/main/java/org/springframework/retry/policy/CompositeRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/CompositeRetryPolicy.java @@ -30,6 +30,7 @@ * * @author Dave Syer * @author Michael Minella + * @author Emanuele Ivaldi * */ @SuppressWarnings("serial") @@ -145,6 +146,22 @@ public void registerThrowable(RetryContext context, Throwable throwable) { ((RetryContextSupport) context).registerThrowable(throwable); } + /** + * @return the lower 'maximum number of attempts before failure' between all policies + * that have a 'maximum number of attempts before failure' set, if at least one is + * present among the policies, return {@link RetryPolicy#NO_MAXIMUM_ATTEMPTS_SET} + * otherwise + */ + @Override + public int getMaxAttempts() { + return Arrays.stream(policies) + .map(RetryPolicy::getMaxAttempts) + .filter(maxAttempts -> maxAttempts != NO_MAXIMUM_ATTEMPTS_SET) + .sorted() + .findFirst() + .orElse(NO_MAXIMUM_ATTEMPTS_SET); + } + private static class CompositeRetryContext extends RetryContextSupport { RetryContext[] contexts; diff --git a/src/main/java/org/springframework/retry/policy/MaxAttemptsRetryPolicy.java b/src/main/java/org/springframework/retry/policy/MaxAttemptsRetryPolicy.java index 78a4de32..06db30df 100644 --- a/src/main/java/org/springframework/retry/policy/MaxAttemptsRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/MaxAttemptsRetryPolicy.java @@ -74,6 +74,7 @@ public void setMaxAttempts(int maxAttempts) { * The maximum number of attempts before failure. * @return the maximum number of attempts */ + @Override public int getMaxAttempts() { return this.maxAttempts; } diff --git a/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java index 3916cc24..bede5057 100644 --- a/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java @@ -58,6 +58,7 @@ * @author Gary Russell * @author Aleksandr Shamukov * @author Artem Bilan + * @author Emanuele Ivaldi */ @SuppressWarnings("serial") public class SimpleRetryPolicy implements RetryPolicy { @@ -194,6 +195,7 @@ public void maxAttemptsSupplier(Supplier maxAttemptsSupplier) { * The maximum number of attempts before failure. * @return the maximum number of attempts */ + @Override public int getMaxAttempts() { if (this.maxAttemptsSupplier != null) { return this.maxAttemptsSupplier.get(); diff --git a/src/main/java/org/springframework/retry/support/RetryTemplate.java b/src/main/java/org/springframework/retry/support/RetryTemplate.java index abc1d90d..ac715fec 100644 --- a/src/main/java/org/springframework/retry/support/RetryTemplate.java +++ b/src/main/java/org/springframework/retry/support/RetryTemplate.java @@ -75,6 +75,7 @@ * @author Artem Bilan * @author Josh Long * @author Aleksandr Shamukov + * @author Emanuele Ivaldi */ public class RetryTemplate implements RetryOperations { @@ -296,6 +297,10 @@ protected T doExecute(RetryCallback retryCallback throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt"); } + if (!context.hasAttribute(RetryContext.MAX_ATTEMPTS)) { + context.setAttribute(RetryContext.MAX_ATTEMPTS, retryPolicy.getMaxAttempts()); + } + // Get or Start the backoff context... BackOffContext backOffContext = null; Object resource = context.getAttribute("backOffContext"); diff --git a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java index 2d20b0f8..a8a3ad99 100644 --- a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java +++ b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java @@ -60,6 +60,7 @@ * @author Henning Pöttker * @author Yanming Zhou * @author Anton Aharkau + * @author Emanuele Ivaldi * @since 1.1 */ public class EnableRetryTests { @@ -295,7 +296,7 @@ void runtimeExpressions() throws Exception { ExpressionService service = context.getBean(ExpressionService.class); service.service6(); RuntimeConfigs runtime = context.getBean(RuntimeConfigs.class); - verify(runtime, times(5)).getMaxAttempts(); + verify(runtime, times(6)).getMaxAttempts(); verify(runtime, times(1)).getInitial(); verify(runtime, times(2)).getMax(); verify(runtime, times(2)).getMult(); diff --git a/src/test/java/org/springframework/retry/policy/CompositeRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/CompositeRetryPolicyTests.java index ed60bd14..4c56209a 100644 --- a/src/test/java/org/springframework/retry/policy/CompositeRetryPolicyTests.java +++ b/src/test/java/org/springframework/retry/policy/CompositeRetryPolicyTests.java @@ -160,4 +160,23 @@ public boolean canRetry(RetryContext context) { assertThat(policy.canRetry(context)).isTrue(); } + @Test + public void testMaximumAttemptsForNonSuitablePolicies() { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setOptimistic(true); + policy.setPolicies(new RetryPolicy[] { new NeverRetryPolicy(), new NeverRetryPolicy() }); + + assertThat(policy.getMaxAttempts()).isEqualTo(RetryPolicy.NO_MAXIMUM_ATTEMPTS_SET); + } + + @Test + public void testMaximumAttemptsForSuitablePolicies() { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setOptimistic(true); + policy.setPolicies( + new RetryPolicy[] { new SimpleRetryPolicy(6), new SimpleRetryPolicy(3), new SimpleRetryPolicy(4) }); + + assertThat(policy.getMaxAttempts()).isEqualTo(3); + } + } diff --git a/src/test/java/org/springframework/retry/support/RetryTemplateTests.java b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java index 104e8273..1828cb3e 100644 --- a/src/test/java/org/springframework/retry/support/RetryTemplateTests.java +++ b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java @@ -26,11 +26,13 @@ import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.RetryListener; +import org.springframework.retry.RetryPolicy; import org.springframework.retry.TerminatedRetryException; import org.springframework.retry.backoff.BackOffContext; import org.springframework.retry.backoff.BackOffInterruptedException; import org.springframework.retry.backoff.BackOffPolicy; import org.springframework.retry.backoff.StatelessBackOffPolicy; +import org.springframework.retry.policy.AlwaysRetryPolicy; import org.springframework.retry.policy.NeverRetryPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; @@ -49,6 +51,7 @@ * @author Dave Syer * @author Gary Russell * @author Henning Pöttker + * @author Emanuele Ivaldi */ public class RetryTemplateTests { @@ -333,6 +336,30 @@ public void onSuccess(RetryContext context, RetryCallba assertThat(callCount.get()).isEqualTo(2); } + @Test + public void testContextForPolicyWithMaximumNumberOfAttempts() throws Throwable { + RetryTemplate retryTemplate = new RetryTemplate(); + RetryPolicy retryPolicy = new SimpleRetryPolicy(2); + retryTemplate.setRetryPolicy(retryPolicy); + + Integer result = retryTemplate.execute((RetryCallback) context -> (Integer) context + .getAttribute(RetryContext.MAX_ATTEMPTS), context -> RetryPolicy.NO_MAXIMUM_ATTEMPTS_SET); + + assertThat(result).isEqualTo(2); + } + + @Test + public void testContextForPolicyWithNoMaximumNumberOfAttempts() throws Throwable { + RetryTemplate retryTemplate = new RetryTemplate(); + RetryPolicy retryPolicy = new AlwaysRetryPolicy(); + retryTemplate.setRetryPolicy(retryPolicy); + + Integer result = retryTemplate.execute((RetryCallback) context -> (Integer) context + .getAttribute(RetryContext.MAX_ATTEMPTS), context -> RetryPolicy.NO_MAXIMUM_ATTEMPTS_SET); + + assertThat(result).isEqualTo(RetryPolicy.NO_MAXIMUM_ATTEMPTS_SET); + } + private static class MockRetryCallback implements RetryCallback { private int attempts;