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