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
This commit is contained in:
@@ -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> {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@@ -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;
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
@Override
|
||||
public boolean shouldRetry(Throwable throwable) {
|
||||
return this.retryAttempts++ < MaxRetryAttemptsPolicy.this.maxRetryAttempts;
|
||||
return (this.retryAttempts++ < MaxRetryAttemptsPolicy.this.maxRetryAttempts);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
Duration currentRetryDuration = Duration.between(this.retryStartTime, LocalDateTime.now());
|
||||
return currentRetryDuration.compareTo(MaxRetryDurationPolicy.this.maxRetryDuration) <= 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
return "greeting service";
|
||||
}
|
||||
};
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
retryTemplate.setRetryPolicy(new MaxRetryAttemptsPolicy());
|
||||
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(exception);
|
||||
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 {
|
||||
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 {
|
||||
};
|
||||
}
|
||||
};
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
retryTemplate.setBackOffPolicy(new FixedBackOff());
|
||||
retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE));
|
||||
|
||||
// when
|
||||
ThrowingCallable throwingCallable = () -> retryTemplate.execute(retryCallback);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -76,4 +76,5 @@ class ComposedRetryListenerTests {
|
||||
verify(this.listener1).onRetryPolicyExhaustion(retryExecution, exception);
|
||||
verify(this.listener2).onRetryPolicyExhaustion(retryExecution, exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
class MaxRetryAttemptsPolicyTests {
|
||||
|
||||
@Test
|
||||
void testDefaultMaxRetryAttempts() {
|
||||
void defaultMaxRetryAttempts() {
|
||||
// given
|
||||
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy();
|
||||
Throwable throwable = mock();
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user