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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<Class<? extends Throwable>> includes = new LinkedHashSet<>();
|
||||
|
||||
private final Set<Class<? extends Throwable>> excludes = new LinkedHashSet<>();
|
||||
|
||||
private @Nullable Predicate<Throwable> 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.
|
||||
* <p>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<? extends Throwable>... types) {
|
||||
Collections.addAll(this.includes, types);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the types of exceptions for which the {@link RetryPolicy}
|
||||
* should not retry a failed operation.
|
||||
* <p>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<? extends Throwable>... 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}.
|
||||
* <p>If a predicate has already been configured, the supplied predicate
|
||||
* will be {@linkplain Predicate#and(Predicate) combined} with the
|
||||
* existing predicate.
|
||||
* <p>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<Throwable> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>Defaults to {@code new MaxRetryAttemptsPolicy()}.
|
||||
* <p>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");
|
||||
|
||||
@@ -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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user