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:
Sam Brannen
2025-06-05 14:55:59 +02:00
parent 3fb4a75ae4
commit 02af9e5cee
16 changed files with 179 additions and 150 deletions

View File

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

View File

@@ -76,4 +76,5 @@ class ComposedRetryListenerTests {
verify(this.listener1).onRetryPolicyExhaustion(retryExecution, exception);
verify(this.listener2).onRetryPolicyExhaustion(retryExecution, exception);
}
}

View File

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

View File

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

View File

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