Browse Source

Revise RetryTemplate for alignment with Reactor

Exposes last exception as cause in RetryException.
Applies first back-off after the initial exception.
Breaks out of retry loop on BackOffExecution.STOP.
Expects null result in Retryable and RetryListener.

Closes gh-35057
pull/35061/head
Juergen Hoeller 8 months ago
parent
commit
945f3fb5ac
  1. 4
      spring-core/src/main/java/org/springframework/core/retry/RetryListener.java
  2. 61
      spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
  3. 4
      spring-core/src/main/java/org/springframework/core/retry/Retryable.java
  4. 8
      spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java

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

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.core.retry;
import org.jspecify.annotations.Nullable;
import org.springframework.core.retry.support.CompositeRetryListener;
/**
@ -42,7 +44,7 @@ public interface RetryListener { @@ -42,7 +44,7 @@ public interface RetryListener {
* @param retryExecution the retry execution
* @param result the result of the {@link Retryable}
*/
default void onRetrySuccess(RetryExecution retryExecution, Object result) {
default void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
}
/**

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

@ -17,14 +17,13 @@ @@ -17,14 +17,13 @@
package org.springframework.core.retry;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogAccessor;
import org.springframework.core.retry.support.CompositeRetryListener;
import org.springframework.core.retry.support.MaxRetryAttemptsPolicy;
import org.springframework.util.Assert;
import org.springframework.util.backoff.BackOff;
@ -48,6 +47,7 @@ import org.springframework.util.backoff.FixedBackOff; @@ -48,6 +47,7 @@ import org.springframework.util.backoff.FixedBackOff;
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @author Juergen Hoeller
* @since 7.0
* @see RetryOperations
* @see RetryPolicy
@ -57,14 +57,13 @@ import org.springframework.util.backoff.FixedBackOff; @@ -57,14 +57,13 @@ import org.springframework.util.backoff.FixedBackOff;
*/
public class RetryTemplate implements RetryOperations {
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
private static final LogAccessor logger = new LogAccessor(RetryTemplate.class);
protected RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
private RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
protected BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
protected RetryListener retryListener = new RetryListener() {
};
private RetryListener retryListener = new RetryListener() {};
/**
@ -121,7 +120,8 @@ public class RetryTemplate implements RetryOperations { @@ -121,7 +120,8 @@ public class RetryTemplate implements RetryOperations {
/**
* Set the {@link RetryListener} to use.
* <p>If multiple listeners are needed, use a {@link CompositeRetryListener}.
* <p>If multiple listeners are needed, use a
* {@link org.springframework.core.retry.support.CompositeRetryListener}.
* <p>Defaults to a <em>no-op</em> implementation.
* @param retryListener the retry listener to use
*/
@ -158,10 +158,26 @@ public class RetryTemplate implements RetryOperations { @@ -158,10 +158,26 @@ public class RetryTemplate implements RetryOperations {
// Retry process starts here
RetryExecution retryExecution = this.retryPolicy.start();
BackOffExecution backOffExecution = this.backOffPolicy.start();
List<Throwable> suppressedExceptions = new ArrayList<>();
Deque<Throwable> exceptions = new ArrayDeque<>();
exceptions.add(initialException);
Throwable retryException = initialException;
while (retryExecution.shouldRetry(retryException)) {
try {
long duration = backOffExecution.nextBackOff();
if (duration == BackOffExecution.STOP) {
break;
}
logger.debug(() -> "Backing off for %dms after retryable operation '%s'"
.formatted(duration, retryableName));
Thread.sleep(duration);
}
catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
throw new RetryException(
"Unable to back off for retryable operation '%s'".formatted(retryableName),
interruptedException);
}
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName));
try {
this.retryListener.beforeRetry(retryExecution);
@ -172,29 +188,22 @@ public class RetryTemplate implements RetryOperations { @@ -172,29 +188,22 @@ public class RetryTemplate implements RetryOperations {
return result;
}
catch (Throwable currentAttemptException) {
logger.debug(() -> "Retry attempt for operation '%s' failed due to '%s'"
.formatted(retryableName, currentAttemptException));
this.retryListener.onRetryFailure(retryExecution, currentAttemptException);
try {
long duration = backOffExecution.nextBackOff();
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 retryable operation '%s'".formatted(retryableName),
interruptedException);
}
suppressedExceptions.add(currentAttemptException);
exceptions.add(currentAttemptException);
retryException = currentAttemptException;
}
}
// 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 operation '%s' exhausted; aborting execution".formatted(retryableName),
initialException);
suppressedExceptions.forEach(finalException::addSuppressed);
exceptions.removeLast());
for (Iterator<Throwable> it = exceptions.descendingIterator(); it.hasNext();) {
finalException.addSuppressed(it.next());
}
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException);
throw finalException;
}

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

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.core.retry;
import org.jspecify.annotations.Nullable;
/**
* {@code Retryable} is a functional interface that can be used to implement any
* generic block of code that can potentially be retried.
@ -36,7 +38,7 @@ public interface Retryable<R> { @@ -36,7 +38,7 @@ public interface Retryable<R> {
* @return the result of the operation
* @throws Throwable if an error occurs during the execution of the operation
*/
R execute() throws Throwable;
@Nullable R execute() throws Throwable;
/**
* A unique, logical name for this retryable operation, used to distinguish

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

@ -19,6 +19,8 @@ package org.springframework.core.retry.support; @@ -19,6 +19,8 @@ package org.springframework.core.retry.support;
import java.util.LinkedList;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.core.retry.RetryExecution;
import org.springframework.core.retry.RetryListener;
import org.springframework.core.retry.RetryTemplate;
@ -26,11 +28,10 @@ import org.springframework.util.Assert; @@ -26,11 +28,10 @@ import org.springframework.util.Assert;
/**
* A composite implementation of the {@link RetryListener} interface.
* Delegate listeners will be called in their registration order.
*
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
*
* <p>Delegate listeners will be called in their registration order.
*
* @author Mahmoud Ben Hassine
* @since 7.0
*/
@ -63,13 +64,14 @@ public class CompositeRetryListener implements RetryListener { @@ -63,13 +64,14 @@ public class CompositeRetryListener implements RetryListener {
this.listeners.add(listener);
}
@Override
public void beforeRetry(RetryExecution retryExecution) {
this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryExecution));
}
@Override
public void onRetrySuccess(RetryExecution retryExecution, Object result) {
public void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
this.listeners.forEach(listener -> listener.onRetrySuccess(retryExecution, result));
}

Loading…
Cancel
Save