Introduce Builder API and factory methods for RetryPolicy

Prior to this commit, we had three concrete RetryPolicy implementations.

- MaxRetryAttemptsPolicy
- MaxDurationAttemptsPolicy
- PredicateRetryPolicy

However, there was no way to combine the behavior of those policies.
Furthermore, the PredicateRetryPolicy was practically useless as a
standalone policy, since it did not have a way to end an infinite loop
for a Retryable that continually throws an exception which matches the
predicate.

This commit therefore replaces the current built-in RetryPolicy
implementations with a fluent Builder API and dedicated factory methods
for common use cases.

In addition, this commit also introduces built-in support for
specifying include/exclude lists.

Examples:

new MaxRetryAttemptsPolicy(5) -->

    RetryPolicy.withMaxAttempts(5)

new MaxDurationAttemptsPolicy(Duration.ofSeconds(5)) -->

    RetryPolicy.withMaxDuration(Duration.ofSeconds(5))

new PredicateRetryPolicy(IOException.class::isInstance) -->

    RetryPolicy.builder()
        .maxAttempts(3)
        .predicate(IOException.class::isInstance)
        .build();

The following example demonstrates all supported features of the builder.

RetryPolicy.builder()
    .maxAttempts(5)
    .maxDuration(Duration.ofMillis(100))
    .includes(IOException.class)
    .excludes(FileNotFoundException.class)
    .predicate(t -> t.getMessage().contains("Unexpected failure"))
    .build();

Closes gh-35058
This commit is contained in:
Sam Brannen
2025-06-16 13:15:38 +02:00
parent 945f3fb5ac
commit 97522cfa36
13 changed files with 713 additions and 537 deletions
@@ -0,0 +1,188 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileSystemException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Max attempts tests for {@link DefaultRetryPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxAttemptsDefaultRetryPolicyTests {
@Test
void invalidMaxAttempts() {
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxAttempts(0))
.withMessage("Max attempts must be greater than zero");
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxAttempts(-1))
.withMessage("Max attempts must be greater than zero");
}
@Test
void maxAttempts() {
var retryPolicy = RetryPolicy.withMaxAttempts(2);
var retryExecution = retryPolicy.start();
var throwable = mock(Throwable.class);
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
}
@Test
void maxAttemptsAndPredicate() {
var retryPolicy = RetryPolicy.builder()
.maxAttempts(4)
.predicate(NumberFormatException.class::isInstance)
.build();
var retryExecution = retryPolicy.start();
// 4 retries
assertThat(retryExecution.shouldRetry(new NumberFormatException())).isTrue();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
assertThat(retryExecution.shouldRetry(new CustomNumberFormatException())).isTrue();
// After policy exhaustion
assertThat(retryExecution.shouldRetry(new NumberFormatException())).isFalse();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
}
@Test
void maxAttemptsWithIncludesAndExcludes() {
var policy = RetryPolicy.builder()
.maxAttempts(6)
.includes(RuntimeException.class, IOException.class)
.excludes(FileNotFoundException.class, CustomFileSystemException.class)
.build();
var retryExecution = policy.start();
// 6 retries
assertThat(retryExecution.shouldRetry(new IOException())).isTrue();
assertThat(retryExecution.shouldRetry(new RuntimeException())).isTrue();
assertThat(retryExecution.shouldRetry(new FileNotFoundException())).isFalse();
assertThat(retryExecution.shouldRetry(new FileSystemException("file"))).isTrue();
assertThat(retryExecution.shouldRetry(new CustomFileSystemException("file"))).isFalse();
assertThat(retryExecution.shouldRetry(new IOException())).isTrue();
// After policy exhaustion
assertThat(retryExecution.shouldRetry(new IOException())).isFalse();
}
@Test
void toStringImplementations() {
var policy = RetryPolicy.builder()
.maxAttempts(6)
.includes(RuntimeException.class, IOException.class)
.excludes(FileNotFoundException.class, CustomFileSystemException.class)
.build();
assertThat(policy).asString().isEqualTo("""
DefaultRetryPolicy[\
maxAttempts=6, \
includes=[java.lang.RuntimeException, java.io.IOException], \
excludes=[java.io.FileNotFoundException, \
org.springframework.core.retry.MaxAttemptsDefaultRetryPolicyTests.CustomFileSystemException]]""");
var template = """
DefaultRetryPolicyExecution[\
maxAttempts=6, \
retryCount=%d, \
includes=[java.lang.RuntimeException, java.io.IOException], \
excludes=[java.io.FileNotFoundException, \
org.springframework.core.retry.MaxAttemptsDefaultRetryPolicyTests.CustomFileSystemException]]""";
var retryExecution = policy.start();
var count = 0;
assertThat(retryExecution).asString().isEqualTo(template, count++);
retryExecution.shouldRetry(new IOException());
assertThat(retryExecution).asString().isEqualTo(template, count++);
retryExecution.shouldRetry(new IOException());
assertThat(retryExecution).asString().isEqualTo(template, count++);
}
@Test
void toStringImplementationsWithPredicateAsClass() {
var policy = RetryPolicy.builder()
.maxAttempts(1)
.predicate(new NumberFormatExceptionMatcher())
.build();
assertThat(policy).asString()
.isEqualTo("DefaultRetryPolicy[maxAttempts=1, predicate=NumberFormatExceptionMatcher]");
var retryExecution = policy.start();
assertThat(retryExecution).asString()
.isEqualTo("DefaultRetryPolicyExecution[maxAttempts=1, retryCount=0, predicate=NumberFormatExceptionMatcher]");
}
@Test
void toStringImplementationsWithPredicateAsLambda() {
var policy = RetryPolicy.builder()
.maxAttempts(2)
.predicate(NumberFormatException.class::isInstance)
.build();
assertThat(policy).asString()
.matches("DefaultRetryPolicy\\[maxAttempts=2, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
var retryExecution = policy.start();
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=0, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
retryExecution.shouldRetry(new NumberFormatException());
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=1, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
retryExecution.shouldRetry(new NumberFormatException());
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=2, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
retryExecution.shouldRetry(new NumberFormatException());
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=3, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
}
@SuppressWarnings("serial")
private static class CustomNumberFormatException extends NumberFormatException {
}
@SuppressWarnings("serial")
private static class CustomFileSystemException extends FileSystemException {
CustomFileSystemException(String file) {
super(file);
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import static java.time.Duration.ofSeconds;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Max duration tests for {@link DefaultRetryPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxDurationDefaultRetryPolicyTests {
@Test
void invalidMaxDuration() {
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxDuration(Duration.ZERO))
.withMessage("Max duration must be positive");
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxDuration(ofSeconds(-1)))
.withMessage("Max duration must be positive");
}
@Test
void toStringImplementations() {
var policy1 = RetryPolicy.withMaxDuration(ofSeconds(3));
var policy2 = RetryPolicy.builder()
.maxDuration(ofSeconds(1))
.predicate(new NumberFormatExceptionMatcher())
.build();
assertThat(policy1).asString()
.isEqualTo("DefaultRetryPolicy[maxDuration=3000ms]");
assertThat(policy2).asString()
.isEqualTo("DefaultRetryPolicy[maxDuration=1000ms, predicate=NumberFormatExceptionMatcher]");
assertThat(policy1.start()).asString()
.matches("DefaultRetryPolicyExecution\\[maxDuration=3000ms, retryStartTime=.+]");
assertThat(policy2.start()).asString()
.matches("DefaultRetryPolicyExecution\\[maxDuration=1000ms, retryStartTime=.+, predicate=NumberFormatExceptionMatcher]");
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry;
import java.util.function.Predicate;
/**
* Predicate that matches {@link NumberFormatException}.
*
* @author Sam Brannen
* @since 7.0
*/
class NumberFormatExceptionMatcher implements Predicate<Throwable> {
@Override
public boolean test(Throwable throwable) {
return (throwable instanceof NumberFormatException);
}
}
@@ -16,6 +16,8 @@
package org.springframework.core.retry;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
@@ -26,6 +28,7 @@ import org.springframework.util.backoff.FixedBackOff;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.InstanceOfAssertFactories.array;
/**
* Tests for {@link RetryTemplate}.
@@ -74,10 +77,10 @@ class RetryTemplateTests {
@Test
void retryWithExhaustedPolicy() {
AtomicInteger invocationCount = new AtomicInteger();
RuntimeException exception = new RuntimeException("Boom!");
var invocationCount = new AtomicInteger();
var exception = new RuntimeException("Boom!");
Retryable<String> retryable = new Retryable<>() {
var retryable = new Retryable<>() {
@Override
public String execute() {
invocationCount.incrementAndGet();
@@ -100,11 +103,11 @@ class RetryTemplateTests {
}
@Test
void retryWithFailingRetryableAndCustomRetryPolicy() {
AtomicInteger invocationCount = new AtomicInteger();
RuntimeException exception = new NumberFormatException();
void retryWithFailingRetryableAndCustomRetryPolicyWithMultiplePredicates() {
var invocationCount = new AtomicInteger();
var exception = new NumberFormatException("Boom!");
Retryable<String> retryable = new Retryable<>() {
var retryable = new Retryable<>() {
@Override
public String execute() {
invocationCount.incrementAndGet();
@@ -117,20 +120,111 @@ class RetryTemplateTests {
}
};
AtomicInteger retryCount = new AtomicInteger();
// Custom RetryPolicy that only retries for a NumberFormatException and max 5 retry attempts.
RetryPolicy retryPolicy = () -> throwable -> (retryCount.incrementAndGet() <= 5 && throwable instanceof NumberFormatException);
var retryPolicy = RetryPolicy.builder()
.maxAttempts(5)
.maxDuration(Duration.ofMillis(100))
.predicate(NumberFormatException.class::isInstance)
.predicate(t -> t.getMessage().equals("Boom!"))
.build();
retryTemplate.setRetryPolicy(retryPolicy);
assertThat(invocationCount).hasValue(0);
assertThat(retryCount).hasValue(0);
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'always fails' exhausted; aborting execution")
.withCause(exception);
// 6 = 1 initial invocation + 5 retry attempts
// 6 = 1 initial invocation + 5 retry attempts
assertThat(invocationCount).hasValue(6);
assertThat(retryCount).hasValue(6);
}
@Test
void retryWithExceptionIncludes() throws Exception {
var invocationCount = new AtomicInteger();
var retryable = new Retryable<>() {
@Override
public String execute() throws Exception {
return switch (invocationCount.incrementAndGet()) {
case 1 -> throw new FileNotFoundException();
case 2 -> throw new IOException();
case 3 -> throw new IllegalStateException();
default -> "success";
};
}
@Override
public String getName() {
return "test";
}
};
var retryPolicy = RetryPolicy.builder()
.maxAttempts(Integer.MAX_VALUE)
.includes(IOException.class)
.build();
retryTemplate.setRetryPolicy(retryPolicy);
assertThat(invocationCount).hasValue(0);
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'test' exhausted; aborting execution")
.withCauseExactlyInstanceOf(IllegalStateException.class)
.extracting(Throwable::getSuppressed, array(Throwable[].class))
.satisfiesExactly(
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(FileNotFoundException.class)
);
// 3 = 1 initial invocation + 2 retry attempts
assertThat(invocationCount).hasValue(3);
}
@Test
void retryWithExceptionExcludes() throws Exception {
var invocationCount = new AtomicInteger();
var retryable = new Retryable<>() {
@Override
public String execute() throws Exception {
return switch (invocationCount.incrementAndGet()) {
case 1 -> throw new IOException();
case 2 -> throw new IOException();
case 3 -> throw new CustomFileNotFoundException();
default -> "success";
};
}
@Override
public String getName() {
return "test";
}
};
var retryPolicy = RetryPolicy.builder()
.maxAttempts(Integer.MAX_VALUE)
.includes(IOException.class)
.excludes(FileNotFoundException.class)
.build();
retryTemplate.setRetryPolicy(retryPolicy);
assertThat(invocationCount).hasValue(0);
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'test' exhausted; aborting execution")
.withCauseExactlyInstanceOf(CustomFileNotFoundException.class)
.extracting(Throwable::getSuppressed, array(Throwable[].class))
.satisfiesExactly(
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
);
// 3 = 1 initial invocation + 2 retry attempts
assertThat(invocationCount).hasValue(3);
}
@SuppressWarnings("serial")
private static class CustomFileNotFoundException extends FileNotFoundException {
}
}
@@ -1,114 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
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.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MaxRetryAttemptsPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxRetryAttemptsPolicyTests {
@Test
void defaultMaxRetryAttempts() {
// given
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy();
Throwable throwable = mock();
// when
RetryExecution retryExecution = retryPolicy.start();
// then
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
}
@Test
void customMaxRetryAttempts() {
// given
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy(2);
Throwable throwable = mock();
// when
RetryExecution retryExecution = retryPolicy.start();
// then
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
}
@Test
void invalidMaxRetryAttempts() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryAttemptsPolicy(0))
.withMessage("Max retry attempts must be greater than zero");
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryAttemptsPolicy(-1))
.withMessage("Max retry attempts must be greater than zero");
}
@Test
void toStringImplementations() {
MaxRetryAttemptsPolicy policy1 = new MaxRetryAttemptsPolicy();
MaxRetryAttemptsPolicy policy2 = new MaxRetryAttemptsPolicy(1);
assertThat(policy1).asString().isEqualTo("MaxRetryAttemptsPolicy[maxRetryAttempts=3]");
assertThat(policy2).asString().isEqualTo("MaxRetryAttemptsPolicy[maxRetryAttempts=1]");
RetryExecution retryExecution = policy1.start();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=0, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isTrue();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=1, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isTrue();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=2, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isTrue();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=3, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isFalse();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=4, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isFalse();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=5, maxRetryAttempts=3]");
}
}
@@ -1,58 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import java.time.Duration;
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.assertThatIllegalArgumentException;
/**
* Tests for {@link MaxRetryDurationPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxRetryDurationPolicyTests {
@Test
void invalidMaxRetryDuration() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryDurationPolicy(Duration.ZERO))
.withMessage("Max retry duration must be positive");
}
@Test
void toStringImplementations() {
MaxRetryDurationPolicy policy1 = new MaxRetryDurationPolicy();
MaxRetryDurationPolicy policy2 = new MaxRetryDurationPolicy(Duration.ofSeconds(1));
assertThat(policy1).asString().isEqualTo("MaxRetryDurationPolicy[maxRetryDuration=3000ms]");
assertThat(policy2).asString().isEqualTo("MaxRetryDurationPolicy[maxRetryDuration=1000ms]");
assertThat(policy1.start()).asString()
.matches("MaxRetryDurationPolicyExecution\\[retryStartTime=.+, maxRetryDuration=3000ms\\]");
assertThat(policy2.start()).asString()
.matches("MaxRetryDurationPolicyExecution\\[retryStartTime=.+, maxRetryDuration=1000ms\\]");
}
}
@@ -1,70 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.springframework.core.retry.RetryExecution;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PredicateRetryPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class PredicateRetryPolicyTests {
@Test
void predicateRetryPolicy() {
Predicate<Throwable> predicate = NumberFormatException.class::isInstance;
PredicateRetryPolicy retryPolicy = new PredicateRetryPolicy(predicate);
RetryExecution retryExecution = retryPolicy.start();
assertThat(retryExecution.shouldRetry(new NumberFormatException())).isTrue();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
}
@Test
void toStringImplementations() {
PredicateRetryPolicy policy1 = new PredicateRetryPolicy(NumberFormatException.class::isInstance);
PredicateRetryPolicy policy2 = new PredicateRetryPolicy(new NumberFormatExceptionMatcher());
assertThat(policy1).asString().matches("PredicateRetryPolicy\\[predicate=PredicateRetryPolicyTests.+?Lambda.+?\\]");
assertThat(policy2).asString().isEqualTo("PredicateRetryPolicy[predicate=NumberFormatExceptionMatcher]");
assertThat(policy1.start()).asString()
.matches("PredicateRetryPolicyExecution\\[predicate=PredicateRetryPolicyTests.+?Lambda.+?\\]");
assertThat(policy2.start()).asString()
.isEqualTo("PredicateRetryPolicyExecution[predicate=NumberFormatExceptionMatcher]");
}
private static class NumberFormatExceptionMatcher implements Predicate<Throwable> {
@Override
public boolean test(Throwable throwable) {
return (throwable instanceof NumberFormatException);
}
}
}