Update RetryPolicySettings to configure exception policy as well

Closes gh-47264
This commit is contained in:
Stéphane Nicoll 2025-10-07 14:07:17 +02:00
parent cb51a44e2b
commit da8eac3483
2 changed files with 147 additions and 0 deletions

View File

@ -17,7 +17,10 @@
package org.springframework.boot.retry; package org.springframework.boot.retry;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
@ -53,6 +56,12 @@ public final class RetryPolicySettings {
*/ */
public static final Duration DEFAULT_MAX_DELAY = Duration.ofMillis(RetryPolicy.Builder.DEFAULT_MAX_DELAY); public static final Duration DEFAULT_MAX_DELAY = Duration.ofMillis(RetryPolicy.Builder.DEFAULT_MAX_DELAY);
private List<Class<? extends Throwable>> exceptionIncludes = new ArrayList<>();
private List<Class<? extends Throwable>> exceptionExcludes = new ArrayList<>();
private @Nullable Predicate<Throwable> exceptionPredicate;
private Long maxAttempts = DEFAULT_MAX_ATTEMPTS; private Long maxAttempts = DEFAULT_MAX_ATTEMPTS;
private Duration delay = DEFAULT_DELAY; private Duration delay = DEFAULT_DELAY;
@ -72,6 +81,9 @@ public final class RetryPolicySettings {
public RetryPolicy createRetryPolicy() { public RetryPolicy createRetryPolicy() {
PropertyMapper map = PropertyMapper.get(); PropertyMapper map = PropertyMapper.get();
RetryPolicy.Builder builder = RetryPolicy.builder(); RetryPolicy.Builder builder = RetryPolicy.builder();
map.from(this::getExceptionIncludes).to(builder::includes);
map.from(this::getExceptionExcludes).to(builder::excludes);
map.from(this::getExceptionPredicate).to(builder::predicate);
map.from(this::getMaxAttempts).to(builder::maxAttempts); map.from(this::getMaxAttempts).to(builder::maxAttempts);
map.from(this::getDelay).to(builder::delay); map.from(this::getDelay).to(builder::delay);
map.from(this::getJitter).to(builder::jitter); map.from(this::getJitter).to(builder::jitter);
@ -80,6 +92,64 @@ public final class RetryPolicySettings {
return (this.factory != null) ? this.factory.apply(builder) : builder.build(); return (this.factory != null) ? this.factory.apply(builder) : builder.build();
} }
/**
* Return the applicable exception types to attempt a retry for.
* <p>
* The default is empty, leading to a retry attempt for any exception.
* @return the applicable exception types
*/
public List<Class<? extends Throwable>> getExceptionIncludes() {
return this.exceptionIncludes;
}
/**
* Replace the applicable exception types to attempt a retry for by the given
* {@code includes}. Alternatively consider using {@link #getExceptionIncludes()} to
* mutate the existing list.
* @param includes the applicable exception types
*/
public void setExceptionIncludes(List<Class<? extends Throwable>> includes) {
this.exceptionIncludes = new ArrayList<>(includes);
}
/**
* Return the non-applicable exception types to avoid a retry for.
* <p>
* The default is empty, leading to a retry attempt for any exception.
* @return the non-applicable exception types
*/
public List<Class<? extends Throwable>> getExceptionExcludes() {
return this.exceptionExcludes;
}
/**
* Replace the non-applicable exception types to attempt a retry for by the given
* {@code excludes}. Alternatively consider using {@link #getExceptionExcludes()} to
* mutate the existing list.
* @param excludes the non-applicable types
*/
public void setExceptionExcludes(List<Class<? extends Throwable>> excludes) {
this.exceptionExcludes = new ArrayList<>(excludes);
}
/**
* Return the predicate to use to determine whether to retry a failed operation based
* on a given {@link Throwable}.
* @return the predicate to use
*/
public @Nullable Predicate<Throwable> getExceptionPredicate() {
return this.exceptionPredicate;
}
/**
* Set the predicate to use to determine whether to retry a failed operation based on
* a given {@link Throwable}.
* @param exceptionPredicate the predicate to use
*/
public void setExceptionPredicate(@Nullable Predicate<Throwable> exceptionPredicate) {
this.exceptionPredicate = exceptionPredicate;
}
/** /**
* Return the maximum number of retry attempts. * Return the maximum number of retry attempts.
* @return the maximum number of retry attempts * @return the maximum number of retry attempts

View File

@ -17,6 +17,9 @@
package org.springframework.boot.retry; package org.springframework.boot.retry;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -25,6 +28,8 @@ import org.springframework.util.backoff.BackOff;
import org.springframework.util.backoff.ExponentialBackOff; import org.springframework.util.backoff.ExponentialBackOff;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -34,6 +39,78 @@ import static org.mockito.Mockito.mock;
*/ */
class RetryPolicySettingsTests { class RetryPolicySettingsTests {
@Test
void exceptionIncludesCanBeReplaced() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.getExceptionIncludes().add(IllegalStateException.class);
settings.setExceptionIncludes(List.of(IllegalArgumentException.class));
assertThat(settings.getExceptionIncludes()).containsExactly(IllegalArgumentException.class);
}
@Test
void exceptionIncludesListIsCopied() {
RetryPolicySettings settings = new RetryPolicySettings();
List<Class<? extends Throwable>> includes = new ArrayList<>();
includes.add(IllegalStateException.class);
settings.setExceptionIncludes(includes);
includes.add(IllegalArgumentException.class);
assertThat(settings.getExceptionIncludes()).containsExactly(IllegalStateException.class);
}
@Test
void createRetryPolicyWithExceptionIncludes() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.getExceptionIncludes().add(IllegalStateException.class);
RetryPolicy retryPolicy = settings.createRetryPolicy();
assertThat(retryPolicy.shouldRetry(new IllegalStateException("test"))).isTrue();
assertThat(retryPolicy.shouldRetry(new IllegalArgumentException("test"))).isFalse();
}
@Test
void exceptionExcludesCanBeReplaced() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.getExceptionExcludes().add(IllegalStateException.class);
settings.setExceptionExcludes(List.of(IllegalArgumentException.class));
assertThat(settings.getExceptionExcludes()).containsExactly(IllegalArgumentException.class);
}
@Test
void exceptionExcludesListIsCopied() {
RetryPolicySettings settings = new RetryPolicySettings();
List<Class<? extends Throwable>> excludes = new ArrayList<>();
excludes.add(IllegalStateException.class);
settings.setExceptionExcludes(excludes);
excludes.add(IllegalArgumentException.class);
assertThat(settings.getExceptionExcludes()).containsExactly(IllegalStateException.class);
}
@Test
void createRetryPolicyWithExceptionExcludes() {
RetryPolicySettings settings = new RetryPolicySettings();
settings.getExceptionExcludes().add(IllegalStateException.class);
RetryPolicy retryPolicy = settings.createRetryPolicy();
assertThat(retryPolicy.shouldRetry(new IllegalStateException("test"))).isFalse();
assertThat(retryPolicy.shouldRetry(new IllegalArgumentException("test"))).isTrue();
}
@Test
void getDefaultExceptionPredicate() {
assertThat(new RetryPolicySettings().getExceptionPredicate()).isNull();
}
@Test
void createRetryPolicyWithExceptionPredicate() {
IllegalArgumentException exception = new IllegalArgumentException("test");
Predicate<Throwable> exceptionPredicate = mock();
given(exceptionPredicate.test(exception)).willReturn(true);
RetryPolicySettings settings = new RetryPolicySettings();
settings.setExceptionPredicate(exceptionPredicate);
RetryPolicy retryPolicy = settings.createRetryPolicy();
assertThat(retryPolicy.shouldRetry(exception)).isTrue();
then(exceptionPredicate).should().test(exception);
then(exceptionPredicate).shouldHaveNoMoreInteractions();
}
@Test @Test
void createRetryPolicyWithDefaultsMatchesBackOffDefaults() { void createRetryPolicyWithDefaultsMatchesBackOffDefaults() {
RetryPolicy defaultRetryPolicy = RetryPolicy.builder().build(); RetryPolicy defaultRetryPolicy = RetryPolicy.builder().build();