parent
f927ff635a
commit
51b6e8cc9f
|
@ -40,7 +40,7 @@ public interface RetryListener {
|
|||
/**
|
||||
* Called after the first successful retry attempt.
|
||||
* @param retryExecution the retry execution
|
||||
* @param result the result of the callback
|
||||
* @param result the result of the {@link Retryable}
|
||||
*/
|
||||
default void onRetrySuccess(RetryExecution retryExecution, Object result) {
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ public interface RetryListener {
|
|||
/**
|
||||
* Called every time a retry attempt fails.
|
||||
* @param retryExecution the retry execution
|
||||
* @param throwable the exception thrown by the callback
|
||||
* @param throwable the exception thrown by the {@link Retryable}
|
||||
*/
|
||||
default void onRetryFailure(RetryExecution retryExecution, Throwable throwable) {
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ public interface RetryListener {
|
|||
/**
|
||||
* Called if the {@link RetryPolicy} is exhausted.
|
||||
* @param retryExecution the retry execution
|
||||
* @param throwable the last exception thrown by the {@link RetryCallback}
|
||||
* @param throwable the last exception thrown by the {@link Retryable}
|
||||
*/
|
||||
default void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) {
|
||||
}
|
||||
|
|
|
@ -31,16 +31,16 @@ import org.jspecify.annotations.Nullable;
|
|||
public interface RetryOperations {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Execute the given {@link Retryable} (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 retryable the {@code Retryable} to execute and retry if needed
|
||||
* @param <R> the type of the result
|
||||
* @return the result of the callback, if any
|
||||
* @return the result of the {@code Retryable}, 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;
|
||||
<R extends @Nullable Object> R execute(Retryable<R> retryable) throws RetryException;
|
||||
|
||||
}
|
||||
|
|
|
@ -32,11 +32,11 @@ import org.springframework.util.backoff.FixedBackOff;
|
|||
|
||||
/**
|
||||
* A basic implementation of {@link RetryOperations} that invokes and potentially
|
||||
* retries a {@link RetryCallback} based on a configured {@link RetryPolicy} and
|
||||
* {@link BackOff} policy.
|
||||
* retries a {@link Retryable} operation based on a configured {@link RetryPolicy}
|
||||
* and {@link BackOff} policy.
|
||||
*
|
||||
* <p>By default, a callback will be invoked at most 3 times with a fixed backoff
|
||||
* of 1 second.
|
||||
* <p>By default, a retryable operation 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
|
||||
|
@ -52,7 +52,7 @@ import org.springframework.util.backoff.FixedBackOff;
|
|||
* @see RetryPolicy
|
||||
* @see BackOff
|
||||
* @see RetryListener
|
||||
* @see RetryCallback
|
||||
* @see Retryable
|
||||
*/
|
||||
public class RetryTemplate implements RetryOperations {
|
||||
|
||||
|
@ -128,29 +128,30 @@ public class RetryTemplate implements RetryOperations {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Execute the supplied {@link Retryable} according to the configured retry
|
||||
* and backoff policies.
|
||||
* <p>If the {@code Retryable} succeeds, its result will be returned. Otherwise,
|
||||
* a {@link RetryException} will be thrown to the caller.
|
||||
* @param retryable the {@code Retryable} to execute and retry if needed
|
||||
* @param <R> the type of the result
|
||||
* @return the result of the callback, if any
|
||||
* @return the result of the {@code Retryable}, 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 {
|
||||
String callbackName = retryCallback.getName();
|
||||
public <R extends @Nullable Object> R execute(Retryable<R> retryable) throws RetryException {
|
||||
String retryableName = retryable.getName();
|
||||
// Initial attempt
|
||||
try {
|
||||
logger.debug(() -> "Preparing to execute callback '" + callbackName + "'");
|
||||
R result = retryCallback.run();
|
||||
logger.debug(() -> "Callback '" + callbackName + "' completed successfully");
|
||||
logger.debug(() -> "Preparing to execute retryable operation '%s'".formatted(retryableName));
|
||||
R result = retryable.run();
|
||||
logger.debug(() -> "Retryable operation '%s' completed successfully".formatted(retryableName));
|
||||
return result;
|
||||
}
|
||||
catch (Throwable initialException) {
|
||||
logger.debug(initialException,
|
||||
() -> "Execution of callback '" + callbackName + "' failed; initiating the retry process");
|
||||
() -> "Execution of retryable operation '%s' failed; initiating the retry process"
|
||||
.formatted(retryableName));
|
||||
// Retry process starts here
|
||||
RetryExecution retryExecution = this.retryPolicy.start();
|
||||
BackOffExecution backOffExecution = this.backOffPolicy.start();
|
||||
|
@ -158,25 +159,27 @@ public class RetryTemplate implements RetryOperations {
|
|||
|
||||
Throwable retryException = initialException;
|
||||
while (retryExecution.shouldRetry(retryException)) {
|
||||
logger.debug(() -> "Preparing to retry callback '" + callbackName + "'");
|
||||
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName));
|
||||
try {
|
||||
this.retryListener.beforeRetry(retryExecution);
|
||||
R result = retryCallback.run();
|
||||
R result = retryable.run();
|
||||
this.retryListener.onRetrySuccess(retryExecution, result);
|
||||
logger.debug(() -> "Callback '" + callbackName + "' completed successfully after retry");
|
||||
logger.debug(() -> "Retryable operation '%s' completed successfully after retry"
|
||||
.formatted(retryableName));
|
||||
return result;
|
||||
}
|
||||
catch (Throwable currentAttemptException) {
|
||||
this.retryListener.onRetryFailure(retryExecution, currentAttemptException);
|
||||
try {
|
||||
long duration = backOffExecution.nextBackOff();
|
||||
logger.debug(() -> "Retry callback '" + callbackName + "' failed due to '" +
|
||||
currentAttemptException.getMessage() + "'; backing off for " + duration + "ms");
|
||||
logger.debug(() -> "Retryable operation '%s' failed due to '%s'; backing off for %dms"
|
||||
.formatted(retryableName, currentAttemptException.getMessage(), duration));
|
||||
Thread.sleep(duration);
|
||||
}
|
||||
catch (InterruptedException interruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RetryException("Unable to back off for retry callback '" + callbackName + "'",
|
||||
throw new RetryException(
|
||||
"Unable to back off for retryable operation '%s'".formatted(retryableName),
|
||||
interruptedException);
|
||||
}
|
||||
suppressedExceptions.add(currentAttemptException);
|
||||
|
@ -185,8 +188,9 @@ public class RetryTemplate implements RetryOperations {
|
|||
}
|
||||
// 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);
|
||||
RetryException finalException = new RetryException(
|
||||
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
|
||||
initialException);
|
||||
suppressedExceptions.forEach(finalException::addSuppressed);
|
||||
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException);
|
||||
throw finalException;
|
||||
|
|
|
@ -17,30 +17,32 @@
|
|||
package org.springframework.core.retry;
|
||||
|
||||
/**
|
||||
* Callback interface for a retryable block of code.
|
||||
* {@code Retryable} is a functional interface that can be used to implement any
|
||||
* generic block of code that can potentially be retried.
|
||||
*
|
||||
* <p>Used in conjunction with {@link RetryOperations}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @param <R> the type of the result
|
||||
* @see RetryOperations
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RetryCallback<R> {
|
||||
public interface Retryable<R> {
|
||||
|
||||
/**
|
||||
* Method to execute and retry if needed.
|
||||
* @return the result of the callback
|
||||
* @throws Throwable if an error occurs during the execution of the callback
|
||||
* @return the result of the operation
|
||||
* @throws Throwable if an error occurs during the execution of the operation
|
||||
*/
|
||||
R run() throws Throwable;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* A unique, logical name for this retryable operation, used to distinguish
|
||||
* between retries for different business operations.
|
||||
* <p>Defaults to the fully-qualified class name of the implementation class.
|
||||
* @return the name of this retryable operation
|
||||
*/
|
||||
default String getName() {
|
||||
return getClass().getName();
|
|
@ -37,7 +37,7 @@ class RetryTemplateTests {
|
|||
|
||||
@Test
|
||||
void retryWithSuccess() throws Exception {
|
||||
RetryCallback<String> retryCallback = new RetryCallback<>() {
|
||||
Retryable<String> retryable = new Retryable<>() {
|
||||
|
||||
int failure;
|
||||
|
||||
|
@ -57,14 +57,14 @@ class RetryTemplateTests {
|
|||
|
||||
retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE));
|
||||
|
||||
assertThat(retryTemplate.execute(retryCallback)).isEqualTo("hello world");
|
||||
assertThat(retryTemplate.execute(retryable)).isEqualTo("hello world");
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryWithFailure() {
|
||||
Exception exception = new Exception("Error while invoking greeting service");
|
||||
|
||||
RetryCallback<String> retryCallback = new RetryCallback<>() {
|
||||
Retryable<String> retryable = new Retryable<>() {
|
||||
@Override
|
||||
public String run() throws Exception {
|
||||
throw exception;
|
||||
|
@ -79,8 +79,8 @@ class RetryTemplateTests {
|
|||
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")
|
||||
.isThrownBy(() -> retryTemplate.execute(retryable))
|
||||
.withMessage("Retry policy for operation 'greeting service' exhausted; aborting execution")
|
||||
.withCause(exception);
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ class RetryTemplateTests {
|
|||
|
||||
TechnicalException technicalException = new TechnicalException("Error while invoking greeting service");
|
||||
|
||||
RetryCallback<String> retryCallback = new RetryCallback<>() {
|
||||
Retryable<String> retryable = new Retryable<>() {
|
||||
@Override
|
||||
public String run() throws TechnicalException {
|
||||
throw technicalException;
|
||||
|
@ -126,8 +126,8 @@ class RetryTemplateTests {
|
|||
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")
|
||||
.isThrownBy(() -> retryTemplate.execute(retryable))
|
||||
.withMessage("Retry policy for operation 'greeting service' exhausted; aborting execution")
|
||||
.withCause(technicalException);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue