diff --git a/spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java new file mode 100644 index 00000000000..e37c5ddad53 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java @@ -0,0 +1,167 @@ +/* + * 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 java.time.LocalDateTime; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * Default {@link RetryPolicy} created by {@link RetryPolicy.Builder}. + * + * @author Sam Brannen + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +class DefaultRetryPolicy implements RetryPolicy { + + private final int maxAttempts; + + private final @Nullable Duration maxDuration; + + private final Set> includes; + + private final Set> excludes; + + private final @Nullable Predicate predicate; + + + DefaultRetryPolicy(int maxAttempts, @Nullable Duration maxDuration, Set> includes, + Set> excludes, @Nullable Predicate predicate) { + + Assert.isTrue((maxAttempts > 0 || maxDuration != null), "Max attempts or max duration must be specified"); + + this.maxAttempts = maxAttempts; + this.maxDuration = maxDuration; + this.includes = includes; + this.excludes = excludes; + this.predicate = predicate; + } + + + @Override + public RetryExecution start() { + return new DefaultRetryPolicyExecution(); + } + + @Override + public String toString() { + StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicy[", "]"); + if (this.maxAttempts > 0) { + result.add("maxAttempts=" + this.maxAttempts); + } + if (this.maxDuration != null) { + result.add("maxDuration=" + this.maxDuration.toMillis() + "ms"); + } + if (!this.includes.isEmpty()) { + result.add("includes=" + names(this.includes)); + } + if (!this.excludes.isEmpty()) { + result.add("excludes=" + names(this.excludes)); + } + if (this.predicate != null) { + result.add("predicate=" + this.predicate.getClass().getSimpleName()); + } + return result.toString(); + } + + + private static String names(Set> types) { + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Class type : types) { + String name = type.getCanonicalName(); + result.add(name != null? name : type.getName()); + } + return result.toString(); + } + + + /** + * {@link RetryExecution} for {@link DefaultRetryPolicy}. + */ + private class DefaultRetryPolicyExecution implements RetryExecution { + + private final LocalDateTime retryStartTime = LocalDateTime.now(); + + private int retryCount; + + + @Override + public boolean shouldRetry(Throwable throwable) { + if (DefaultRetryPolicy.this.maxAttempts > 0 && + this.retryCount++ >= DefaultRetryPolicy.this.maxAttempts) { + return false; + } + if (DefaultRetryPolicy.this.maxDuration != null) { + Duration retryDuration = Duration.between(this.retryStartTime, LocalDateTime.now()); + if (retryDuration.compareTo(DefaultRetryPolicy.this.maxDuration) > 0) { + return false; + } + } + if (!DefaultRetryPolicy.this.excludes.isEmpty()) { + for (Class excludedType : DefaultRetryPolicy.this.excludes) { + if (excludedType.isInstance(throwable)) { + return false; + } + } + } + if (!DefaultRetryPolicy.this.includes.isEmpty()) { + boolean included = false; + for (Class includedType : DefaultRetryPolicy.this.includes) { + if (includedType.isInstance(throwable)) { + included = true; + break; + } + } + if (!included) { + return false; + } + } + return DefaultRetryPolicy.this.predicate == null || DefaultRetryPolicy.this.predicate.test(throwable); + } + + @Override + public String toString() { + StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicyExecution[", "]"); + if (DefaultRetryPolicy.this.maxAttempts > 0) { + result.add("maxAttempts=" + DefaultRetryPolicy.this.maxAttempts); + result.add("retryCount=" + this.retryCount); + } + if (DefaultRetryPolicy.this.maxDuration != null) { + result.add("maxDuration=" + DefaultRetryPolicy.this.maxDuration.toMillis() + "ms"); + result.add("retryStartTime=" + this.retryStartTime); + } + if (!DefaultRetryPolicy.this.includes.isEmpty()) { + result.add("includes=" + names(DefaultRetryPolicy.this.includes)); + } + if (!DefaultRetryPolicy.this.excludes.isEmpty()) { + result.add("excludes=" + names(DefaultRetryPolicy.this.excludes)); + } + if (DefaultRetryPolicy.this.predicate != null) { + result.add("predicate=" + DefaultRetryPolicy.this.predicate.getClass().getSimpleName()); + } + return result.toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java index d7335fb9143..240c0419682 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java @@ -16,11 +16,28 @@ package org.springframework.core.retry; +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + /** * Strategy interface to define a retry policy. * + *

Also provides factory methods and a fluent builder API for creating retry + * policies with common configurations. See {@link #withMaxAttempts(int)}, + * {@link #withMaxDuration(Duration)}, {@link #builder()}, and the configuration + * options in {@link Builder} for details. + * + * @author Sam Brannen * @author Mahmoud Ben Hassine * @since 7.0 + * @see RetryExecution */ public interface RetryPolicy { @@ -30,4 +47,133 @@ public interface RetryPolicy { */ RetryExecution start(); + + /** + * Create a {@link RetryPolicy} configured with a maximum number of retry attempts. + * @param maxAttempts the maximum number of retry attempts; must be greater than zero + * @see Builder#maxAttempts(int) + */ + static RetryPolicy withMaxAttempts(int maxAttempts) { + return builder().maxAttempts(maxAttempts).build(); + } + + /** + * Create a {@link RetryPolicy} configured with a maximum retry {@link Duration}. + * @param maxDuration the maximum retry duration; must be positive + * @see Builder#maxDuration(Duration) + */ + static RetryPolicy withMaxDuration(Duration maxDuration) { + return builder().maxDuration(maxDuration).build(); + } + + /** + * Create a {@link Builder} to configure a {@link RetryPolicy} with common + * configuration options. + */ + static Builder builder() { + return new Builder(); + } + + + /** + * Fluent API for configuring a {@link RetryPolicy} with common configuration + * options. + */ + final class Builder { + + private int maxAttempts; + + private @Nullable Duration maxDuration; + + private final Set> includes = new LinkedHashSet<>(); + + private final Set> excludes = new LinkedHashSet<>(); + + private @Nullable Predicate predicate; + + + private Builder() { + // internal constructor + } + + + /** + * Specify the maximum number of retry attempts. + * @param maxAttempts the maximum number of retry attempts; must be + * greater than zero + * @return this {@code Builder} instance for chained method invocations + */ + public Builder maxAttempts(int maxAttempts) { + Assert.isTrue(maxAttempts > 0, "Max attempts must be greater than zero"); + this.maxAttempts = maxAttempts; + return this; + } + + /** + * Specify the maximum retry {@link Duration}. + * @param maxDuration the maximum retry duration; must be positive + * @return this {@code Builder} instance for chained method invocations + */ + public Builder maxDuration(Duration maxDuration) { + Assert.isTrue(!maxDuration.isNegative() && !maxDuration.isZero(), "Max duration must be positive"); + this.maxDuration = maxDuration; + return this; + } + + /** + * Specify the types of exceptions for which the {@link RetryPolicy} + * should retry a failed operation. + *

This can be combined with {@link #excludes(Class...)} and + * {@link #predicate(Predicate)}. + * @param types the types of exceptions to include in the policy + * @return this {@code Builder} instance for chained method invocations + */ + @SafeVarargs // Making the method final allows us to use @SafeVarargs. + @SuppressWarnings("varargs") + public final Builder includes(Class... types) { + Collections.addAll(this.includes, types); + return this; + } + + /** + * Specify the types of exceptions for which the {@link RetryPolicy} + * should not retry a failed operation. + *

This can be combined with {@link #includes(Class...)} and + * {@link #predicate(Predicate)}. + * @param types the types of exceptions to exclude from the policy + * @return this {@code Builder} instance for chained method invocations + */ + @SafeVarargs // Making the method final allows us to use @SafeVarargs. + @SuppressWarnings("varargs") + public final Builder excludes(Class... types) { + Collections.addAll(this.excludes, types); + return this; + } + + /** + * Specify a custom {@link Predicate} that the {@link RetryPolicy} will + * use to determine whether to retry a failed operation based on a given + * {@link Throwable}. + *

If a predicate has already been configured, the supplied predicate + * will be {@linkplain Predicate#and(Predicate) combined} with the + * existing predicate. + *

This can be combined with {@link #includes(Class...)} and + * {@link #excludes(Class...)}. + * @param predicate a custom predicate + * @return this {@code Builder} instance for chained method invocations + */ + public Builder predicate(Predicate predicate) { + this.predicate = (this.predicate != null ? this.predicate.and(predicate) : predicate); + return this; + } + + /** + * Build the {@link RetryPolicy} configured via this {@code Builder}. + */ + public RetryPolicy build() { + return new DefaultRetryPolicy(this.maxAttempts, this.maxDuration, + this.includes, this.excludes, this.predicate); + } + } + } diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java index 6b806fd4d2d..5c0871901aa 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java @@ -24,7 +24,6 @@ import java.util.Iterator; import org.jspecify.annotations.Nullable; import org.springframework.core.log.LogAccessor; -import org.springframework.core.retry.support.MaxRetryAttemptsPolicy; import org.springframework.util.Assert; import org.springframework.util.backoff.BackOff; import org.springframework.util.backoff.BackOffExecution; @@ -59,7 +58,8 @@ public class RetryTemplate implements RetryOperations { private static final LogAccessor logger = new LogAccessor(RetryTemplate.class); - private RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy(); + + private RetryPolicy retryPolicy = RetryPolicy.withMaxAttempts(3); private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1)); @@ -98,9 +98,11 @@ public class RetryTemplate implements RetryOperations { /** * Set the {@link RetryPolicy} to use. - *

Defaults to {@code new MaxRetryAttemptsPolicy()}. + *

Defaults to {@code RetryPolicy.withMaxAttempts(3)}. * @param retryPolicy the retry policy to use - * @see MaxRetryAttemptsPolicy + * @see RetryPolicy#withMaxAttempts(int) + * @see RetryPolicy#withMaxDuration(Duration) + * @see RetryPolicy#builder() */ public void setRetryPolicy(RetryPolicy retryPolicy) { Assert.notNull(retryPolicy, "Retry policy must not be null"); diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.java deleted file mode 100644 index f2733953913..00000000000 --- a/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.java +++ /dev/null @@ -1,100 +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.springframework.core.retry.RetryExecution; -import org.springframework.core.retry.RetryPolicy; -import org.springframework.util.Assert; - -/** - * A {@link RetryPolicy} based on a maximum number of retry attempts. - * - * @author Mahmoud Ben Hassine - * @author Sam Brannen - * @since 7.0 - */ -public final class MaxRetryAttemptsPolicy implements RetryPolicy { - - /** - * The default maximum number of retry attempts: {@value}. - */ - public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 3; - - - private int maxRetryAttempts = DEFAULT_MAX_RETRY_ATTEMPTS; - - - /** - * Create a new {@code MaxRetryAttemptsPolicy} with the default maximum number - * of retry attempts. - * @see #DEFAULT_MAX_RETRY_ATTEMPTS - */ - public MaxRetryAttemptsPolicy() { - } - - /** - * Create a new {@code MaxRetryAttemptsPolicy} with the specified maximum number - * of retry attempts. - * @param maxRetryAttempts the maximum number of retry attempts; must be greater - * than zero - */ - public MaxRetryAttemptsPolicy(int maxRetryAttempts) { - setMaxRetryAttempts(maxRetryAttempts); - } - - /** - * Set the maximum number of retry attempts. - * @param maxRetryAttempts the maximum number of retry attempts; must be greater - * than zero - */ - public void setMaxRetryAttempts(int maxRetryAttempts) { - Assert.isTrue(maxRetryAttempts > 0, "Max retry attempts must be greater than zero"); - this.maxRetryAttempts = maxRetryAttempts; - } - - @Override - public RetryExecution start() { - return new MaxRetryAttemptsPolicyExecution(); - } - - @Override - public String toString() { - return "MaxRetryAttemptsPolicy[maxRetryAttempts=%d]".formatted(this.maxRetryAttempts); - } - - - /** - * A {@link RetryExecution} based on a maximum number of retry attempts. - */ - private class MaxRetryAttemptsPolicyExecution implements RetryExecution { - - private int retryAttempts; - - @Override - public boolean shouldRetry(Throwable throwable) { - return (this.retryAttempts++ < MaxRetryAttemptsPolicy.this.maxRetryAttempts); - } - - @Override - public String toString() { - return "MaxRetryAttemptsPolicyExecution[retryAttempts=%d, maxRetryAttempts=%d]" - .formatted(this.retryAttempts, MaxRetryAttemptsPolicy.this.maxRetryAttempts); - } - - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java deleted file mode 100644 index 104b20d7ba6..00000000000 --- a/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java +++ /dev/null @@ -1,103 +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 java.time.LocalDateTime; - -import org.springframework.core.retry.RetryExecution; -import org.springframework.core.retry.RetryPolicy; -import org.springframework.util.Assert; - -/** - * A {@link RetryPolicy} based on a maximum retry {@link Duration}. - * - * @author Mahmoud Ben Hassine - * @author Sam Brannen - * @since 7.0 - */ -public final class MaxRetryDurationPolicy implements RetryPolicy { - - /** - * The default maximum retry duration: 3 seconds. - */ - public static final Duration DEFAULT_MAX_RETRY_DURATION = Duration.ofSeconds(3); - - - private Duration maxRetryDuration = DEFAULT_MAX_RETRY_DURATION; - - - /** - * Create a new {@code MaxRetryDurationPolicy} with the default maximum retry - * {@link Duration}. - * @see #DEFAULT_MAX_RETRY_DURATION - */ - public MaxRetryDurationPolicy() { - } - - /** - * Create a new {@code MaxRetryDurationPolicy} with the specified maximum retry - * {@link Duration}. - * @param maxRetryDuration the maximum retry duration; must be positive - */ - public MaxRetryDurationPolicy(Duration maxRetryDuration) { - setMaxRetryDuration(maxRetryDuration); - } - - /** - * Set the maximum retry {@link Duration}. - * @param maxRetryDuration the maximum retry duration; must be positive - */ - public void setMaxRetryDuration(Duration maxRetryDuration) { - Assert.isTrue(!maxRetryDuration.isNegative() && !maxRetryDuration.isZero(), - "Max retry duration must be positive"); - this.maxRetryDuration = maxRetryDuration; - } - - @Override - public RetryExecution start() { - return new MaxRetryDurationPolicyExecution(); - } - - @Override - public String toString() { - return "MaxRetryDurationPolicy[maxRetryDuration=%dms]".formatted(this.maxRetryDuration.toMillis()); - } - - - /** - * A {@link RetryExecution} based on a maximum retry duration. - */ - private class MaxRetryDurationPolicyExecution implements RetryExecution { - - private final LocalDateTime retryStartTime = LocalDateTime.now(); - - @Override - public boolean shouldRetry(Throwable throwable) { - Duration currentRetryDuration = Duration.between(this.retryStartTime, LocalDateTime.now()); - return currentRetryDuration.compareTo(MaxRetryDurationPolicy.this.maxRetryDuration) <= 0; - } - - @Override - public String toString() { - return "MaxRetryDurationPolicyExecution[retryStartTime=%s, maxRetryDuration=%dms]" - .formatted(this.retryStartTime, MaxRetryDurationPolicy.this.maxRetryDuration.toMillis()); - } - - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java deleted file mode 100644 index 01bb717a093..00000000000 --- a/spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java +++ /dev/null @@ -1,75 +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.springframework.core.retry.RetryExecution; -import org.springframework.core.retry.RetryPolicy; - -/** - * A {@link RetryPolicy} based on a {@link Predicate}. - * - * @author Mahmoud Ben Hassine - * @author Sam Brannen - * @since 7.0 - */ -public final class PredicateRetryPolicy implements RetryPolicy { - - private final Predicate predicate; - - - /** - * Create a new {@code PredicateRetryPolicy} with the given {@link Predicate}. - * @param predicate the predicate to use for determining whether to retry an - * operation based on a given {@link Throwable} - */ - public PredicateRetryPolicy(Predicate predicate) { - this.predicate = predicate; - } - - - @Override - public RetryExecution start() { - return new PredicateRetryPolicyExecution(); - } - - @Override - public String toString() { - return "PredicateRetryPolicy[predicate=%s]".formatted(this.predicate.getClass().getSimpleName()); - } - - - /** - * A {@link RetryExecution} based on a {@link Predicate}. - */ - private class PredicateRetryPolicyExecution implements RetryExecution { - - @Override - public boolean shouldRetry(Throwable throwable) { - return PredicateRetryPolicy.this.predicate.test(throwable); - } - - @Override - public String toString() { - return "PredicateRetryPolicyExecution[predicate=%s]" - .formatted(PredicateRetryPolicy.this.predicate.getClass().getSimpleName()); - } - - } - -} diff --git a/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java new file mode 100644 index 00000000000..0f05e4b2512 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java @@ -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); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java new file mode 100644 index 00000000000..7b3cda6d3c6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java @@ -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]"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java b/spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java new file mode 100644 index 00000000000..3249d49982e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java @@ -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 { + + @Override + public boolean test(Throwable throwable) { + return (throwable instanceof NumberFormatException); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java index adc3a45c032..eba8887f1fd 100644 --- a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java +++ b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java @@ -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 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 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 { } } diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java deleted file mode 100644 index db1ce6ed164..00000000000 --- a/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java +++ /dev/null @@ -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]"); - } - -} diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java deleted file mode 100644 index a46a4f52c80..00000000000 --- a/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java +++ /dev/null @@ -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\\]"); - } - -} diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java deleted file mode 100644 index e9b16e95c09..00000000000 --- a/spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java +++ /dev/null @@ -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 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 { - - @Override - public boolean test(Throwable throwable) { - return (throwable instanceof NumberFormatException); - } - } - -}