Browse Source
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
pull/35061/head
13 changed files with 713 additions and 537 deletions
@ -0,0 +1,167 @@
@@ -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<Class<? extends Throwable>> includes; |
||||
|
||||
private final Set<Class<? extends Throwable>> excludes; |
||||
|
||||
private final @Nullable Predicate<Throwable> predicate; |
||||
|
||||
|
||||
DefaultRetryPolicy(int maxAttempts, @Nullable Duration maxDuration, Set<Class<? extends Throwable>> includes, |
||||
Set<Class<? extends Throwable>> excludes, @Nullable Predicate<Throwable> 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<Class<? extends Throwable>> types) { |
||||
StringJoiner result = new StringJoiner(", ", "[", "]"); |
||||
for (Class<? extends Throwable> 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<? extends Throwable> excludedType : DefaultRetryPolicy.this.excludes) { |
||||
if (excludedType.isInstance(throwable)) { |
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
if (!DefaultRetryPolicy.this.includes.isEmpty()) { |
||||
boolean included = false; |
||||
for (Class<? extends Throwable> 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(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,100 +0,0 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,103 +0,0 @@
@@ -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()); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,75 +0,0 @@
@@ -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<Throwable> 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<Throwable> 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()); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,188 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -1,114 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue