Rename RetryCallback to Retryable

See gh-34716
This commit is contained in:
Sam Brannen 2025-06-10 18:26:52 +02:00
parent f927ff635a
commit 51b6e8cc9f
5 changed files with 56 additions and 50 deletions

View File

@ -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) {
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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();

View File

@ -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);
}