Browse Source

Revise core retry support

This commit constitutes a first pass over the new core retry support.

- Fix code and Javadoc formatting

- Polish/fix Javadoc

- Fix default FixedBackOff configuration in RetryTemplate

- Consistent logging in RetryTemplate

- Fix listener handling in CompositeRetryListener, allowing addListener()
  to work

- Polish tests

- Ensure RetryTemplateTests do not take over 30 seconds to execute

See gh-34716
pull/35004/head
Sam Brannen 7 months ago
parent
commit
02af9e5cee
  1. 12
      spring-core/src/main/java/org/springframework/core/retry/RetryCallback.java
  2. 13
      spring-core/src/main/java/org/springframework/core/retry/RetryException.java
  3. 3
      spring-core/src/main/java/org/springframework/core/retry/RetryExecution.java
  4. 6
      spring-core/src/main/java/org/springframework/core/retry/RetryListener.java
  5. 17
      spring-core/src/main/java/org/springframework/core/retry/RetryOperations.java
  6. 4
      spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java
  7. 92
      spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
  8. 21
      spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java
  9. 28
      spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.java
  10. 23
      spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java
  11. 14
      spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java
  12. 73
      spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java
  13. 1
      spring-core/src/test/java/org/springframework/core/retry/support/ComposedRetryListenerTests.java
  14. 12
      spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java
  15. 10
      spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java
  16. 4
      spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java

12
spring-core/src/main/java/org/springframework/core/retry/RetryCallback.java

@ -17,7 +17,9 @@ @@ -17,7 +17,9 @@
package org.springframework.core.retry;
/**
* Callback interface for a retryable piece of code. Used in conjunction with {@link RetryOperations}.
* Callback interface for a retryable block of code.
*
* <p>Used in conjunction with {@link RetryOperations}.
*
* @author Mahmoud Ben Hassine
* @since 7.0
@ -35,11 +37,13 @@ public interface RetryCallback<R> { @@ -35,11 +37,13 @@ public interface RetryCallback<R> {
R run() throws Throwable;
/**
* A unique logical name for this callback to distinguish retries around
* business operations.
* @return the name of the callback. Defaults to the class name.
* A unique, logical name for this callback, used to distinguish retries for
* different business operations.
* <p>Defaults to the fully-qualified class name.
* @return the name of the callback
*/
default String getName() {
return getClass().getName();
}
}

13
spring-core/src/main/java/org/springframework/core/retry/RetryException.java

@ -19,7 +19,7 @@ package org.springframework.core.retry; @@ -19,7 +19,7 @@ package org.springframework.core.retry;
import java.io.Serial;
/**
* Exception class for exhausted retries.
* Exception thrown when a {@link RetryPolicy} has been exhausted.
*
* @author Mahmoud Ben Hassine
* @since 7.0
@ -30,18 +30,19 @@ public class RetryException extends Exception { @@ -30,18 +30,19 @@ public class RetryException extends Exception {
@Serial
private static final long serialVersionUID = 5439915454935047936L;
/**
* Create a new exception with a message.
* @param message the exception's message
* Create a new {@code RetryException} for the supplied message.
* @param message the detail message
*/
public RetryException(String message) {
super(message);
}
/**
* Create a new exception with a message and a cause.
* @param message the exception's message
* @param cause the exception's cause
* Create a new {@code RetryException} for the supplied message and cause.
* @param message the detail message
* @param cause the root cause
*/
public RetryException(String message, Throwable cause) {
super(message, cause);

3
spring-core/src/main/java/org/springframework/core/retry/RetryExecution.java

@ -17,7 +17,8 @@ @@ -17,7 +17,8 @@
package org.springframework.core.retry;
/**
* Strategy interface to define a retry execution.
* Strategy interface to define a retry execution created for a given
* {@link RetryPolicy}.
*
* <p>Implementations may be stateful but do not need to be thread-safe.
*

6
spring-core/src/main/java/org/springframework/core/retry/RetryListener.java

@ -48,15 +48,15 @@ public interface RetryListener { @@ -48,15 +48,15 @@ public interface RetryListener {
/**
* Called every time a retry attempt fails.
* @param retryExecution the retry execution
* @param throwable the throwable thrown by the callback
* @param throwable the exception thrown by the callback
*/
default void onRetryFailure(RetryExecution retryExecution, Throwable throwable) {
}
/**
* Called once the retry policy is exhausted.
* Called if the {@link RetryPolicy} is exhausted.
* @param retryExecution the retry execution
* @param throwable the last throwable thrown by the callback
* @param throwable the last exception thrown by the {@link RetryCallback}
*/
default void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) {
}

17
spring-core/src/main/java/org/springframework/core/retry/RetryOperations.java

@ -19,7 +19,7 @@ package org.springframework.core.retry; @@ -19,7 +19,7 @@ package org.springframework.core.retry;
import org.jspecify.annotations.Nullable;
/**
* Main entry point to the core retry functionality. Defines a set of retryable operations.
* Interface specifying basic retry operations.
*
* <p>Implemented by {@link RetryTemplate}. Not often used directly, but a useful
* option to enhance testability, as it can easily be mocked or stubbed.
@ -31,15 +31,16 @@ import org.jspecify.annotations.Nullable; @@ -31,15 +31,16 @@ import org.jspecify.annotations.Nullable;
public interface RetryOperations {
/**
* Retry the given callback (according to the retry policy configured at the implementation level)
* until it succeeds or eventually throw an exception if the retry policy is exhausted.
* Execute the given callback (according to the {@link RetryPolicy} configured
* at the implementation level) until it succeeds, or eventually throw an
* exception if the {@code RetryPolicy} is exhausted.
* @param retryCallback the callback to call initially and retry if needed
* @param <R> the type of the callback's result
* @return the callback's result
* @throws RetryException thrown if the retry policy is exhausted. All attempt exceptions
* should be added as suppressed exceptions to the final exception.
* @param <R> the type of the result
* @return the result of the callback, if any
* @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions
* encountered during retry attempts should be made available as suppressed
* exceptions
*/
<R extends @Nullable Object> R execute(RetryCallback<R> retryCallback) throws RetryException;
}

4
spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java

@ -25,8 +25,8 @@ package org.springframework.core.retry; @@ -25,8 +25,8 @@ package org.springframework.core.retry;
public interface RetryPolicy {
/**
* Start a new retry execution.
* @return a fresh {@link RetryExecution} ready to be used
* Start a new execution for this retry policy.
* @return a new {@link RetryExecution}
*/
RetryExecution start();

92
spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java

@ -31,23 +31,28 @@ import org.springframework.util.backoff.BackOffExecution; @@ -31,23 +31,28 @@ import org.springframework.util.backoff.BackOffExecution;
import org.springframework.util.backoff.FixedBackOff;
/**
* A basic implementation of {@link RetryOperations} that uses a
* {@link RetryPolicy} and a {@link BackOff} to retry a
* {@link RetryCallback}. By default, the callback will be called
* 3 times with a fixed backoff of 1 second.
* A basic implementation of {@link RetryOperations} that invokes and potentially
* retries a {@link RetryCallback} based on a configured {@link RetryPolicy} and
* {@link BackOff} policy.
*
* <p>It is also possible to register a {@link RetryListener} to intercept and inject code
* during key retry phases (before a retry attempt, after a retry attempt, etc.).
* <p>By default, a callback will be invoked at most 3 times with a fixed backoff
* of 1 second.
*
* <p>A {@link RetryListener} can be {@linkplain #setRetryListener(RetryListener)
* registered} to intercept and inject behavior during key retry phases (before a
* retry attempt, after a retry attempt, etc.).
*
* <p>All retry operations performed by this class are logged at debug level,
* using "org.springframework.core.retry.RetryTemplate" as log category.
* using {@code "org.springframework.core.retry.RetryTemplate"} as the log category.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
* @see RetryOperations
* @see RetryPolicy
* @see BackOff
* @see RetryListener
* @see RetryCallback
*/
public class RetryTemplate implements RetryOperations {
@ -55,28 +60,31 @@ public class RetryTemplate implements RetryOperations { @@ -55,28 +60,31 @@ public class RetryTemplate implements RetryOperations {
protected RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
protected BackOff backOffPolicy = new FixedBackOff();
protected BackOff backOffPolicy = new FixedBackOff(1000, Long.MAX_VALUE);
protected RetryListener retryListener = new RetryListener() {
};
/**
* Create a new retry template with default settings.
* Create a new {@code RetryTemplate} with maximum 3 retry attempts and a
* fixed backoff of 1 second.
*/
public RetryTemplate() {
}
/**
* Create a new retry template with a custom {@link RetryPolicy}.
* Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and a
* fixed backoff of 1 second.
* @param retryPolicy the retry policy to use
*/
public RetryTemplate(RetryPolicy retryPolicy) {
Assert.notNull(retryPolicy, "Retry policy must not be null");
Assert.notNull(retryPolicy, "RetryPolicy must not be null");
this.retryPolicy = retryPolicy;
}
/**
* Create a new retry template with a custom {@link RetryPolicy} and {@link BackOff}.
* Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and
* {@link BackOff} policy.
* @param retryPolicy the retry policy to use
* @param backOffPolicy the backoff policy to use
*/
@ -87,8 +95,10 @@ public class RetryTemplate implements RetryOperations { @@ -87,8 +95,10 @@ public class RetryTemplate implements RetryOperations {
}
/**
* Set the {@link RetryPolicy} to use. Defaults to <code>MaxAttemptsRetryPolicy()</code>.
* @param retryPolicy the retry policy to use. Must not be <code>null</code>.
* Set the {@link RetryPolicy} to use.
* <p>Defaults to {@code new MaxRetryAttemptsPolicy()}.
* @param retryPolicy the retry policy to use
* @see MaxRetryAttemptsPolicy
*/
public void setRetryPolicy(RetryPolicy retryPolicy) {
Assert.notNull(retryPolicy, "Retry policy must not be null");
@ -96,8 +106,10 @@ public class RetryTemplate implements RetryOperations { @@ -96,8 +106,10 @@ public class RetryTemplate implements RetryOperations {
}
/**
* Set the {@link BackOff} to use. Defaults to <code>FixedBackOffPolicy(Duration.ofSeconds(1))</code>.
* @param backOffPolicy the backoff policy to use. Must not be <code>null</code>.
* Set the {@link BackOff} policy to use.
* <p>Defaults to {@code new FixedBackOff(1000, Long.MAX_VALUE))}.
* @param backOffPolicy the backoff policy to use
* @see FixedBackOff
*/
public void setBackOffPolicy(BackOff backOffPolicy) {
Assert.notNull(backOffPolicy, "BackOff policy must not be null");
@ -105,9 +117,10 @@ public class RetryTemplate implements RetryOperations { @@ -105,9 +117,10 @@ public class RetryTemplate implements RetryOperations {
}
/**
* Set the {@link RetryListener} to use. Defaults to a <code>NoOp</code> implementation.
* If multiple listeners are needed, use a {@link CompositeRetryListener}.
* @param retryListener the retry listener to use. Must not be <code>null</code>.
* Set the {@link RetryListener} to use.
* <p>If multiple listeners are needed, use a {@link CompositeRetryListener}.
* <p>Defaults to a <em>no-op</em> implementation.
* @param retryListener the retry listener to use
*/
public void setRetryListener(RetryListener retryListener) {
Assert.notNull(retryListener, "Retry listener must not be null");
@ -115,60 +128,65 @@ public class RetryTemplate implements RetryOperations { @@ -115,60 +128,65 @@ public class RetryTemplate implements RetryOperations {
}
/**
* Call the retry callback according to the configured retry and backoff policies.
* If the callback succeeds, its result is returned. Otherwise, a {@link RetryException}
* will be thrown to the caller having all attempt exceptions as suppressed exceptions.
* Execute the supplied {@link RetryCallback} according to the configured
* retry and backoff policies.
* <p>If the callback succeeds, its result will be returned. Otherwise, a
* {@link RetryException} will be thrown to the caller.
* @param retryCallback the callback to call initially and retry if needed
* @param <R> the type of the result
* @return the result of the callback if any
* @throws RetryException thrown if the retry policy is exhausted. All attempt exceptions
* are added as suppressed exceptions to the final exception.
* @return the result of the callback, if any
* @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions
* encountered during retry attempts are available as suppressed exceptions
*/
@Override
public <R extends @Nullable Object> R execute(RetryCallback<R> retryCallback) throws RetryException {
Assert.notNull(retryCallback, "Retry Callback must not be null");
String callbackName = retryCallback.getName();
// initial attempt
// Initial attempt
try {
logger.debug(() -> "About to execute callback '" + callbackName + "'");
logger.debug(() -> "Preparing to execute callback '" + callbackName + "'");
R result = retryCallback.run();
logger.debug(() -> "Callback '" + callbackName + "' executed successfully");
logger.debug(() -> "Callback '" + callbackName + "' completed successfully");
return result;
}
catch (Throwable initialException) {
logger.debug(initialException, () -> "Execution of callback '" + callbackName + "' failed, initiating the retry process");
// retry process starts here
logger.debug(initialException,
() -> "Execution of callback '" + callbackName + "' failed; initiating the retry process");
// Retry process starts here
RetryExecution retryExecution = this.retryPolicy.start();
BackOffExecution backOffExecution = this.backOffPolicy.start();
List<Throwable> suppressedExceptions = new ArrayList<>();
Throwable retryException = initialException;
while (retryExecution.shouldRetry(retryException)) {
logger.debug(() -> "About to retry callback '" + callbackName + "'");
logger.debug(() -> "Preparing to retry callback '" + callbackName + "'");
try {
this.retryListener.beforeRetry(retryExecution);
R result = retryCallback.run();
this.retryListener.onRetrySuccess(retryExecution, result);
logger.debug(() -> "Callback '" + callbackName + "' retried successfully");
logger.debug(() -> "Callback '" + callbackName + "' completed successfully after retry");
return result;
}
catch (Throwable currentAttemptException) {
this.retryListener.onRetryFailure(retryExecution, currentAttemptException);
try {
long duration = backOffExecution.nextBackOff();
logger.debug(() -> "Retry callback '" + callbackName + "' failed for " + currentAttemptException.getMessage() + ", backing off for " + duration + "ms");
logger.debug(() -> "Retry callback '" + callbackName + "' failed due to '" +
currentAttemptException.getMessage() + "'; backing off for " + duration + "ms");
Thread.sleep(duration);
}
catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
throw new RetryException("Unable to backoff for retry callback '" + callbackName + "'", interruptedException);
throw new RetryException("Unable to back off for retry callback '" + callbackName + "'",
interruptedException);
}
suppressedExceptions.add(currentAttemptException);
retryException = currentAttemptException;
}
}
// retry policy exhausted at this point, throwing a RetryException with the initial exception as cause and remaining attempts exceptions as suppressed
RetryException finalException = new RetryException("Retry policy for callback '" + callbackName + "' exhausted, aborting execution", initialException);
// The RetryPolicy has exhausted at this point, so we throw a RetryException with the
// initial exception as the cause and remaining exceptions as suppressed exceptions.
RetryException finalException = new RetryException("Retry policy for callback '" + callbackName +
"' exhausted; aborting execution", initialException);
suppressedExceptions.forEach(finalException::addSuppressed);
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException);
throw finalException;

21
spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java

@ -25,8 +25,9 @@ import org.springframework.core.retry.RetryTemplate; @@ -25,8 +25,9 @@ import org.springframework.core.retry.RetryTemplate;
import org.springframework.util.Assert;
/**
* A composite implementation of the {@link RetryListener} interface. This class
* is used to compose multiple listeners within a {@link RetryTemplate}.
* A composite implementation of the {@link RetryListener} interface.
*
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
*
* <p>Delegate listeners will be called in their registration order.
*
@ -35,30 +36,30 @@ import org.springframework.util.Assert; @@ -35,30 +36,30 @@ import org.springframework.util.Assert;
*/
public class CompositeRetryListener implements RetryListener {
private final List<RetryListener> listeners;
private final List<RetryListener> listeners = new LinkedList<>();
/**
* Create a new {@link CompositeRetryListener}.
* Create a new {@code CompositeRetryListener}.
*/
public CompositeRetryListener() {
this.listeners = new LinkedList<>();
}
/**
* Create a new {@link CompositeRetryListener} with a list of delegates.
* @param listeners the delegate listeners to register. Must not be empty.
* Create a new {@code CompositeRetryListener} with the supplied list of
* delegates.
* @param listeners the list of delegate listeners to register; must not be empty
*/
public CompositeRetryListener(List<RetryListener> listeners) {
Assert.notEmpty(listeners, "RetryListener List must not be empty");
this.listeners = List.copyOf(listeners);
this.listeners.addAll(listeners);
}
/**
* Add a new listener to the list of delegates.
* @param listener the listener to add. Must not be <code>null</code>.
* @param listener the listener to add
*/
public void addListener(RetryListener listener) {
Assert.notNull(listener, "Retry listener must not be null");
this.listeners.add(listener);
}

28
spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.java

@ -21,7 +21,8 @@ import org.springframework.core.retry.RetryPolicy; @@ -21,7 +21,8 @@ import org.springframework.core.retry.RetryPolicy;
import org.springframework.util.Assert;
/**
* A {@link RetryPolicy} based on a number of attempts that should not exceed a maximum number.
* A {@link RetryPolicy} based on a number of attempts that should not exceed a
* configured maximum number.
*
* @author Mahmoud Ben Hassine
* @since 7.0
@ -29,30 +30,32 @@ import org.springframework.util.Assert; @@ -29,30 +30,32 @@ import org.springframework.util.Assert;
public class MaxRetryAttemptsPolicy implements RetryPolicy {
/**
* The default maximum number of retry attempts.
* The default maximum number of retry attempts: {@value}.
*/
public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 3;
private int maxRetryAttempts = DEFAULT_MAX_RETRY_ATTEMPTS;
/**
* Create a new {@link MaxRetryAttemptsPolicy} with the default maximum number of retry attempts.
* Create a new {@code MaxRetryAttemptsPolicy} with the default maximum number
* of retry attempts.
* @see #DEFAULT_MAX_RETRY_ATTEMPTS
*/
public MaxRetryAttemptsPolicy() {
}
/**
* Create a new {@link MaxRetryAttemptsPolicy} with the specified maximum number of retry attempts.
* @param maxRetryAttempts the maximum number of retry attempts. Must be greater than zero.
* Create a new {@code MaxRetryAttemptsPolicy} with the specified maximum number
* of retry attempts.
* @param maxRetryAttempts the maximum number of retry attempts; must be greater
* than zero
*/
public MaxRetryAttemptsPolicy(int maxRetryAttempts) {
setMaxRetryAttempts(maxRetryAttempts);
}
/**
* Start a new retry execution.
* @return a fresh {@link MaxRetryAttemptsPolicyExecution} ready to be used
*/
@Override
public RetryExecution start() {
return new MaxRetryAttemptsPolicyExecution();
@ -60,13 +63,15 @@ public class MaxRetryAttemptsPolicy implements RetryPolicy { @@ -60,13 +63,15 @@ public class MaxRetryAttemptsPolicy implements RetryPolicy {
/**
* Set the maximum number of retry attempts.
* @param maxRetryAttempts the maximum number of retry attempts. Must be greater than zero.
* @param maxRetryAttempts the maximum number of retry attempts; must be greater
* than zero
*/
public void setMaxRetryAttempts(int maxRetryAttempts) {
Assert.isTrue(maxRetryAttempts > 0, "Max retry attempts must be greater than zero");
this.maxRetryAttempts = maxRetryAttempts;
}
/**
* A {@link RetryExecution} based on a maximum number of retry attempts.
*/
@ -76,9 +81,8 @@ public class MaxRetryAttemptsPolicy implements RetryPolicy { @@ -76,9 +81,8 @@ public class MaxRetryAttemptsPolicy implements RetryPolicy {
@Override
public boolean shouldRetry(Throwable throwable) {
return this.retryAttempts++ < MaxRetryAttemptsPolicy.this.maxRetryAttempts;
return (this.retryAttempts++ < MaxRetryAttemptsPolicy.this.maxRetryAttempts);
}
}
}

23
spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java

@ -24,7 +24,7 @@ import org.springframework.core.retry.RetryPolicy; @@ -24,7 +24,7 @@ import org.springframework.core.retry.RetryPolicy;
import org.springframework.util.Assert;
/**
* A {@link RetryPolicy} based on a timeout.
* A {@link RetryPolicy} based on a maximum retry duration.
*
* @author Mahmoud Ben Hassine
* @since 7.0
@ -32,30 +32,31 @@ import org.springframework.util.Assert; @@ -32,30 +32,31 @@ import org.springframework.util.Assert;
public class MaxRetryDurationPolicy implements RetryPolicy {
/**
* The default maximum retry duration.
* The default maximum retry duration: 3 seconds.
*/
public static final Duration DEFAULT_MAX_RETRY_DURATION = Duration.ofSeconds(3);
private Duration maxRetryDuration = DEFAULT_MAX_RETRY_DURATION;
/**
* Create a new {@link MaxRetryDurationPolicy} with the default maximum retry duration.
* Create a new {@code MaxRetryDurationPolicy} with the default maximum retry
* duration.
* @see #DEFAULT_MAX_RETRY_DURATION
*/
public MaxRetryDurationPolicy() {
}
/**
* Create a new {@link MaxRetryDurationPolicy} with the specified maximum retry duration.
* @param maxRetryDuration the maximum retry duration. Must be positive.
* Create a new {@code MaxRetryDurationPolicy} with the specified maximum retry
* duration.
* @param maxRetryDuration the maximum retry duration; must be positive
*/
public MaxRetryDurationPolicy(Duration maxRetryDuration) {
setMaxRetryDuration(maxRetryDuration);
}
/**
* Start a new retry execution.
* @return a fresh {@link MaxRetryDurationPolicyExecution} ready to be used
*/
@Override
public RetryExecution start() {
return new MaxRetryDurationPolicyExecution();
@ -63,7 +64,7 @@ public class MaxRetryDurationPolicy implements RetryPolicy { @@ -63,7 +64,7 @@ public class MaxRetryDurationPolicy implements RetryPolicy {
/**
* Set the maximum retry duration.
* @param maxRetryDuration the maximum retry duration. Must be positive.
* @param maxRetryDuration the maximum retry duration; must be positive
*/
public void setMaxRetryDuration(Duration maxRetryDuration) {
Assert.isTrue(!maxRetryDuration.isNegative() && !maxRetryDuration.isZero(),
@ -83,6 +84,6 @@ public class MaxRetryDurationPolicy implements RetryPolicy { @@ -83,6 +84,6 @@ public class MaxRetryDurationPolicy implements RetryPolicy {
Duration currentRetryDuration = Duration.between(this.retryStartTime, LocalDateTime.now());
return currentRetryDuration.compareTo(MaxRetryDurationPolicy.this.maxRetryDuration) <= 0;
}
}
}

14
spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java

@ -22,7 +22,7 @@ import org.springframework.core.retry.RetryExecution; @@ -22,7 +22,7 @@ import org.springframework.core.retry.RetryExecution;
import org.springframework.core.retry.RetryPolicy;
/**
* A {@link RetryPolicy} based on a predicate.
* A {@link RetryPolicy} based on a {@link Predicate}.
*
* @author Mahmoud Ben Hassine
* @since 7.0
@ -31,18 +31,18 @@ public class PredicateRetryPolicy implements RetryPolicy { @@ -31,18 +31,18 @@ public class PredicateRetryPolicy implements RetryPolicy {
private final Predicate<Throwable> predicate;
/**
* Create a new {@link PredicateRetryPolicy} with the given predicate.
* @param predicate the predicate to use for determining whether to retry the exception or not
* Create a new {@code PredicateRetryPolicy} with the given predicate.
* @param predicate the predicate to use for determining whether to retry an
* operation based on a given {@link Throwable}
*/
public PredicateRetryPolicy(Predicate<Throwable> predicate) {
this.predicate = predicate;
}
/**
* Start a new retry execution.
* @return a fresh {@link RetryExecution} ready to be used
*/
@Override
public RetryExecution start() {
return this.predicate::test;
}

73
spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java

@ -16,27 +16,31 @@ @@ -16,27 +16,31 @@
package org.springframework.core.retry;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Test;
import org.springframework.core.retry.support.MaxRetryAttemptsPolicy;
import org.springframework.util.backoff.FixedBackOff;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link RetryTemplate}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class RetryTemplateTests {
private final RetryTemplate retryTemplate = new RetryTemplate();
@Test
void testRetryWithSuccess() throws Exception {
// given
void retryWithSuccess() throws Exception {
RetryCallback<String> retryCallback = new RetryCallback<>() {
int failure;
@Override
public String run() throws Exception {
if (failure++ < 2) {
@ -50,21 +54,16 @@ class RetryTemplateTests { @@ -50,21 +54,16 @@ class RetryTemplateTests {
return "greeting service";
}
};
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new MaxRetryAttemptsPolicy());
retryTemplate.setBackOffPolicy(new FixedBackOff());
// when
String result = retryTemplate.execute(retryCallback);
retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE));
// then
assertThat(result).isEqualTo("hello world");
assertThat(retryTemplate.execute(retryCallback)).isEqualTo("hello world");
}
@Test
void testRetryWithFailure() {
// given
void retryWithFailure() {
Exception exception = new Exception("Error while invoking greeting service");
RetryCallback<String> retryCallback = new RetryCallback<>() {
@Override
public String run() throws Exception {
@ -76,31 +75,27 @@ class RetryTemplateTests { @@ -76,31 +75,27 @@ class RetryTemplateTests {
return "greeting service";
}
};
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new MaxRetryAttemptsPolicy());
retryTemplate.setBackOffPolicy(new FixedBackOff());
// when
ThrowingCallable throwingCallable = () -> retryTemplate.execute(retryCallback);
// then
assertThatThrownBy(throwingCallable)
.isInstanceOf(RetryException.class)
.hasMessage("Retry policy for callback 'greeting service' exhausted, aborting execution")
.hasCause(exception);
retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE));
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryCallback))
.withMessage("Retry policy for callback 'greeting service' exhausted; aborting execution")
.withCause(exception);
}
@Test
void testRetrySpecificException() {
// given
void retrySpecificException() {
@SuppressWarnings("serial")
class TechnicalException extends Exception {
@java.io.Serial
private static final long serialVersionUID = 1L;
public TechnicalException(String message) {
super(message);
}
}
final TechnicalException technicalException = new TechnicalException("Error while invoking greeting service");
TechnicalException technicalException = new TechnicalException("Error while invoking greeting service");
RetryCallback<String> retryCallback = new RetryCallback<>() {
@Override
public String run() throws TechnicalException {
@ -112,11 +107,14 @@ class RetryTemplateTests { @@ -112,11 +107,14 @@ class RetryTemplateTests {
return "greeting service";
}
};
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy() {
@Override
public RetryExecution start() {
return new RetryExecution() {
int retryAttempts;
@Override
public boolean shouldRetry(Throwable throwable) {
return this.retryAttempts++ < 3 && throwable instanceof TechnicalException;
@ -124,18 +122,13 @@ class RetryTemplateTests { @@ -124,18 +122,13 @@ class RetryTemplateTests {
};
}
};
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(new FixedBackOff());
// when
ThrowingCallable throwingCallable = () -> retryTemplate.execute(retryCallback);
retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE));
// then
assertThatThrownBy(throwingCallable)
.isInstanceOf(RetryException.class)
.hasMessage("Retry policy for callback 'greeting service' exhausted, aborting execution")
.hasCause(technicalException);
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryCallback))
.withMessage("Retry policy for callback 'greeting service' exhausted; aborting execution")
.withCause(technicalException);
}
}

1
spring-core/src/test/java/org/springframework/core/retry/support/ComposedRetryListenerTests.java

@ -76,4 +76,5 @@ class ComposedRetryListenerTests { @@ -76,4 +76,5 @@ class ComposedRetryListenerTests {
verify(this.listener1).onRetryPolicyExhaustion(retryExecution, exception);
verify(this.listener2).onRetryPolicyExhaustion(retryExecution, exception);
}
}

12
spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java

@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test; @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.retry.RetryExecution;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
@ -32,7 +32,7 @@ import static org.mockito.Mockito.mock; @@ -32,7 +32,7 @@ import static org.mockito.Mockito.mock;
class MaxRetryAttemptsPolicyTests {
@Test
void testDefaultMaxRetryAttempts() {
void defaultMaxRetryAttempts() {
// given
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy();
Throwable throwable = mock();
@ -48,8 +48,10 @@ class MaxRetryAttemptsPolicyTests { @@ -48,8 +48,10 @@ class MaxRetryAttemptsPolicyTests {
}
@Test
void testInvalidMaxRetryAttempts() {
assertThatThrownBy(() -> new MaxRetryAttemptsPolicy(-1))
.hasMessage("Max retry attempts must be greater than zero");
void invalidMaxRetryAttempts() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryAttemptsPolicy(-1))
.withMessage("Max retry attempts must be greater than zero");
}
}

10
spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java

@ -20,7 +20,7 @@ import java.time.Duration; @@ -20,7 +20,7 @@ import java.time.Duration;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link MaxRetryDurationPolicy}.
@ -30,8 +30,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -30,8 +30,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
class MaxRetryDurationPolicyTests {
@Test
void testInvalidMaxRetryDuration() {
assertThatThrownBy(() -> new MaxRetryDurationPolicy(Duration.ZERO))
.hasMessage("Max retry duration must be positive");
void invalidMaxRetryDuration() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryDurationPolicy(Duration.ZERO))
.withMessage("Max retry duration must be positive");
}
}

4
spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java

@ -32,13 +32,13 @@ import static org.assertj.core.api.Assertions.assertThat; @@ -32,13 +32,13 @@ import static org.assertj.core.api.Assertions.assertThat;
class PredicateRetryPolicyTests {
@Test
void testPredicateRetryPolicy() {
void predicateRetryPolicy() {
// given
class MyException extends Exception {
@java.io.Serial
private static final long serialVersionUID = 1L;
}
Predicate<Throwable> predicate = throwable -> throwable instanceof MyException;
Predicate<Throwable> predicate = MyException.class::isInstance;
PredicateRetryPolicy retryPolicy = new PredicateRetryPolicy(predicate);
// when

Loading…
Cancel
Save