Redesign RetryPolicy to directly incorporate BackOff
After experimenting with our newly introduced core retry support (RetryPolicy, RetryTemplate, etc.) and @Retryable support, it became apparent that there are overlapping concerns between the current RetryPolicy and BackOff contracts. - RetryPolicy and BackOff both have stateful executions: RetryExecution and BackOffExecution. However, only one stateful execution is necessary. - FixedBackOff and ExponentialBackOff already incorporate "retry" logic in terms of max attempts, max elapsed time, etc. Thus, there is no need to duplicate such behavior in a RetryPolicy and its RetryExecution. - RetryTemplate currently accepts both a RetryPolicy and a BackOff in order to instrument the retry algorithm. However, users would probably rather focus on configuring all "retry" logic via a single mechanism. In light of the above, this commit directly incorporates BackOff in RetryPolicy as follows. - Remove the RetryExecution interface and move its shouldRetry() method to RetryPolicy, replacing the current RetryExecution start() method. - Introduce a default getBackOff() method in the RetryPolicy interface. - Introduce RetryPolicy.withDefaults() factory method. - Completely overhaul the RetryPolicy.Builder to provide support for configuring a BackOff strategy. - Remove BackOff configuration from RetryTemplate. - Revise the method signatures of callbacks in RetryListener. The collective result of these changes can be witnessed in the reworked implementation of AbstractRetryInterceptor. RetryPolicy retryPolicy = RetryPolicy.builder() .includes(spec.includes()) .excludes(spec.excludes()) .predicate(spec.predicate().forMethod(method)) .maxAttempts(spec.maxAttempts()) .delay(Duration.ofMillis(spec.delay())) .maxDelay(Duration.ofMillis(spec.maxDelay())) .jitter(Duration.ofMillis(spec.jitter())) .multiplier(spec.multiplier()) .build(); RetryTemplate retryTemplate = new RetryTemplate(retryPolicy); See gh-34716 See gh-34529 See gh-35058 Closes gh-35110
This commit is contained in:
parent
5a6c019413
commit
bcdf26d492
|
@ -34,7 +34,6 @@ import org.springframework.core.retry.RetryPolicy;
|
|||
import org.springframework.core.retry.RetryTemplate;
|
||||
import org.springframework.core.retry.Retryable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.backoff.ExponentialBackOff;
|
||||
|
||||
/**
|
||||
* Abstract retry interceptor implementation, adapting a given
|
||||
|
@ -89,26 +88,17 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
|||
}
|
||||
}
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
RetryPolicy.Builder policyBuilder = RetryPolicy.builder();
|
||||
for (Class<? extends Throwable> include : spec.includes()) {
|
||||
policyBuilder.includes(include);
|
||||
}
|
||||
for (Class<? extends Throwable> exclude : spec.excludes()) {
|
||||
policyBuilder.excludes(exclude);
|
||||
}
|
||||
policyBuilder.predicate(spec.predicate().forMethod(method));
|
||||
policyBuilder.maxAttempts(spec.maxAttempts());
|
||||
retryTemplate.setRetryPolicy(policyBuilder.build());
|
||||
|
||||
ExponentialBackOff backOff = new ExponentialBackOff();
|
||||
backOff.setInitialInterval(spec.delay());
|
||||
backOff.setJitter(spec.jitter());
|
||||
backOff.setMultiplier(spec.multiplier());
|
||||
backOff.setMaxInterval(spec.maxDelay());
|
||||
backOff.setMaxAttempts(spec.maxAttempts());
|
||||
retryTemplate.setBackOffPolicy(backOff);
|
||||
RetryPolicy retryPolicy = RetryPolicy.builder()
|
||||
.includes(spec.includes())
|
||||
.excludes(spec.excludes())
|
||||
.predicate(spec.predicate().forMethod(method))
|
||||
.maxAttempts(spec.maxAttempts())
|
||||
.delay(Duration.ofMillis(spec.delay()))
|
||||
.maxDelay(Duration.ofMillis(spec.maxDelay()))
|
||||
.jitter(Duration.ofMillis(spec.jitter()))
|
||||
.multiplier(spec.multiplier())
|
||||
.build();
|
||||
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
|
||||
|
||||
try {
|
||||
return retryTemplate.execute(new Retryable<>() {
|
||||
|
|
|
@ -16,15 +16,13 @@
|
|||
|
||||
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;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
|
||||
/**
|
||||
* Default {@link RetryPolicy} created by {@link RetryPolicy.Builder}.
|
||||
|
@ -35,44 +33,58 @@ import org.springframework.util.Assert;
|
|||
*/
|
||||
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;
|
||||
|
||||
private final BackOff backOff;
|
||||
|
||||
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;
|
||||
DefaultRetryPolicy(Set<Class<? extends Throwable>> includes, Set<Class<? extends Throwable>> excludes,
|
||||
@Nullable Predicate<Throwable> predicate, BackOff backOff) {
|
||||
|
||||
this.includes = includes;
|
||||
this.excludes = excludes;
|
||||
this.predicate = predicate;
|
||||
this.backOff = backOff;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RetryExecution start() {
|
||||
return new DefaultRetryPolicyExecution();
|
||||
public boolean shouldRetry(Throwable throwable) {
|
||||
if (!this.excludes.isEmpty()) {
|
||||
for (Class<? extends Throwable> excludedType : this.excludes) {
|
||||
if (excludedType.isInstance(throwable)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.includes.isEmpty()) {
|
||||
boolean included = false;
|
||||
for (Class<? extends Throwable> includedType : this.includes) {
|
||||
if (includedType.isInstance(throwable)) {
|
||||
included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!included) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.predicate == null || this.predicate.test(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BackOff getBackOff() {
|
||||
return this.backOff;
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
@ -82,6 +94,7 @@ class DefaultRetryPolicy implements RetryPolicy {
|
|||
if (this.predicate != null) {
|
||||
result.add("predicate=" + this.predicate.getClass().getSimpleName());
|
||||
}
|
||||
result.add("backOff=" + this.backOff);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
|
@ -95,73 +108,4 @@ class DefaultRetryPolicy implements RetryPolicy {
|
|||
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,40 +0,0 @@
|
|||
/*
|
||||
* Copyright 2002-present 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;
|
||||
|
||||
/**
|
||||
* Strategy interface to define a retry execution created for a given
|
||||
* {@link RetryPolicy}.
|
||||
*
|
||||
* <p>A {@code RetryExecution} is effectively an executable instance of a given
|
||||
* {@code RetryPolicy}.
|
||||
*
|
||||
* <p>Implementations may be stateful but do not need to be thread-safe.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @since 7.0
|
||||
*/
|
||||
public interface RetryExecution {
|
||||
|
||||
/**
|
||||
* Specify if the operation should be retried based on the given throwable.
|
||||
* @param throwable the exception that caused the operation to fail
|
||||
* @return {@code true} if the operation should be retried, {@code false} otherwise
|
||||
*/
|
||||
boolean shouldRetry(Throwable throwable);
|
||||
|
||||
}
|
|
@ -21,12 +21,14 @@ import org.jspecify.annotations.Nullable;
|
|||
import org.springframework.core.retry.support.CompositeRetryListener;
|
||||
|
||||
/**
|
||||
* An extension point that allows to inject code during key retry phases.
|
||||
* {@code RetryListener} defines a <em>listener</em> API for reacting to events
|
||||
* published during the execution of a {@link Retryable} operation.
|
||||
*
|
||||
* <p>Typically registered in a {@link RetryTemplate}, and can be composed using
|
||||
* a {@link CompositeRetryListener}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @see CompositeRetryListener
|
||||
*/
|
||||
|
@ -34,33 +36,37 @@ public interface RetryListener {
|
|||
|
||||
/**
|
||||
* Called before every retry attempt.
|
||||
* @param retryExecution the retry execution
|
||||
* @param retryPolicy the {@link RetryPolicy}
|
||||
* @param retryable the {@link Retryable} operation
|
||||
*/
|
||||
default void beforeRetry(RetryExecution retryExecution) {
|
||||
default void beforeRetry(RetryPolicy retryPolicy, Retryable<?> retryable) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the first successful retry attempt.
|
||||
* @param retryExecution the retry execution
|
||||
* @param result the result of the {@link Retryable}
|
||||
* @param retryPolicy the {@link RetryPolicy}
|
||||
* @param retryable the {@link Retryable} operation
|
||||
* @param result the result of the {@code Retryable} operation
|
||||
*/
|
||||
default void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
|
||||
default void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, @Nullable Object result) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every time a retry attempt fails.
|
||||
* @param retryExecution the retry execution
|
||||
* @param throwable the exception thrown by the {@link Retryable}
|
||||
* @param retryPolicy the {@link RetryPolicy}
|
||||
* @param retryable the {@link Retryable} operation
|
||||
* @param throwable the exception thrown by the {@code Retryable} operation
|
||||
*/
|
||||
default void onRetryFailure(RetryExecution retryExecution, Throwable throwable) {
|
||||
default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if the {@link RetryPolicy} is exhausted.
|
||||
* @param retryExecution the retry execution
|
||||
* @param throwable the last exception thrown by the {@link Retryable}
|
||||
* @param retryPolicy the {@code RetryPolicy}
|
||||
* @param retryable the {@code Retryable} operation
|
||||
* @param throwable the last exception thrown by the {@link Retryable} operation
|
||||
*/
|
||||
default void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) {
|
||||
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.core.retry;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
@ -25,45 +26,82 @@ import java.util.function.Predicate;
|
|||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
import org.springframework.util.backoff.ExponentialBackOff;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* policies with common configurations. See {@link #withDefaults()},
|
||||
* {@link #withMaxAttempts(int)}, {@link #withMaxElapsedTime(Duration)},
|
||||
* {@link #builder()}, and the configuration options in {@link Builder} for details.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @since 7.0
|
||||
* @see RetryExecution
|
||||
* @see Retryable
|
||||
* @see RetryTemplate
|
||||
* @see BackOff
|
||||
*/
|
||||
public interface RetryPolicy {
|
||||
|
||||
/**
|
||||
* Start a new execution for this retry policy.
|
||||
* @return a new {@link RetryExecution}
|
||||
* Specify if the {@link Retryable} operation should be retried based on the
|
||||
* given throwable.
|
||||
* @param throwable the exception that caused the operation to fail
|
||||
* @return {@code true} if the operation should be retried, {@code false} otherwise
|
||||
*/
|
||||
RetryExecution start();
|
||||
boolean shouldRetry(Throwable throwable);
|
||||
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* Get the {@link BackOff} strategy to use for this retry policy.
|
||||
* <p>Defaults to a fixed backoff of {@value Builder#DEFAULT_DELAY} milliseconds
|
||||
* and maximum {@value Builder#DEFAULT_MAX_ATTEMPTS} retry attempts.
|
||||
* @return the {@code BackOff} strategy to use
|
||||
* @see FixedBackOff
|
||||
*/
|
||||
static RetryPolicy withMaxAttempts(int maxAttempts) {
|
||||
return builder().maxAttempts(maxAttempts).build();
|
||||
default BackOff getBackOff() {
|
||||
return new FixedBackOff(Builder.DEFAULT_DELAY, Builder.DEFAULT_MAX_ATTEMPTS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link RetryPolicy} with default configuration.
|
||||
* <p>The returned policy applies to all exception types, uses a fixed backoff
|
||||
* of {@value Builder#DEFAULT_DELAY} milliseconds, and supports maximum
|
||||
* {@value Builder#DEFAULT_MAX_ATTEMPTS} retry attempts.
|
||||
* @see FixedBackOff
|
||||
*/
|
||||
static RetryPolicy withDefaults() {
|
||||
return throwable -> true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RetryPolicy} configured with a maximum retry {@link Duration}.
|
||||
* @param maxDuration the maximum retry duration; must be positive
|
||||
* @see Builder#maxDuration(Duration)
|
||||
* Create a {@link RetryPolicy} configured with a maximum number of retry attempts.
|
||||
* <p>The returned policy uses a fixed backoff of {@value Builder#DEFAULT_DELAY}
|
||||
* milliseconds.
|
||||
* @param maxAttempts the maximum number of retry attempts; must be greater than zero
|
||||
* @see Builder#maxAttempts(int)
|
||||
* @see FixedBackOff
|
||||
*/
|
||||
static RetryPolicy withMaxDuration(Duration maxDuration) {
|
||||
return builder().maxDuration(maxDuration).build();
|
||||
static RetryPolicy withMaxAttempts(int maxAttempts) {
|
||||
Assert.isTrue(maxAttempts > 0, "Max attempts must be greater than zero");
|
||||
return builder().backOff(new FixedBackOff(Builder.DEFAULT_DELAY, maxAttempts)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RetryPolicy} configured with a maximum elapsed time.
|
||||
* <p>The returned policy uses a fixed backoff of {@value Builder#DEFAULT_DELAY}
|
||||
* milliseconds.
|
||||
* @param maxElapsedTime the maximum elapsed time; must be positive
|
||||
* @see Builder#maxElapsedTime(Duration)
|
||||
* @see FixedBackOff
|
||||
*/
|
||||
static RetryPolicy withMaxElapsedTime(Duration maxElapsedTime) {
|
||||
return builder().maxElapsedTime(maxElapsedTime).build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,9 +119,41 @@ public interface RetryPolicy {
|
|||
*/
|
||||
final class Builder {
|
||||
|
||||
/**
|
||||
* The default {@linkplain #maxAttempts(int) max attempts}: {@value}.
|
||||
*/
|
||||
public static final int DEFAULT_MAX_ATTEMPTS = 3;
|
||||
|
||||
/**
|
||||
* The default {@linkplain #delay(Duration) delay}: {@value} ms.
|
||||
*/
|
||||
public static final long DEFAULT_DELAY = 1000;
|
||||
|
||||
/**
|
||||
* The default {@linkplain #maxDelay(Duration) max delay}: {@value} ms.
|
||||
* @see Long#MAX_VALUE
|
||||
*/
|
||||
public static final long DEFAULT_MAX_DELAY = Long.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* The default {@linkplain #multiplier(double) multiplier}: {@value}.
|
||||
*/
|
||||
public static final double DEFAULT_MULTIPLIER = 1.0;
|
||||
|
||||
|
||||
private @Nullable BackOff backOff;
|
||||
|
||||
private int maxAttempts;
|
||||
|
||||
private @Nullable Duration maxDuration;
|
||||
private @Nullable Duration delay;
|
||||
|
||||
private @Nullable Duration jitter;
|
||||
|
||||
private double multiplier;
|
||||
|
||||
private @Nullable Duration maxDelay;
|
||||
|
||||
private @Nullable Duration maxElapsedTime;
|
||||
|
||||
private final Set<Class<? extends Throwable>> includes = new LinkedHashSet<>();
|
||||
|
||||
|
@ -97,10 +167,30 @@ public interface RetryPolicy {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specify the {@link BackOff} strategy to use.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p><strong>WARNING</strong>: If you configure a custom {@code BackOff}
|
||||
* strategy, you should not configure any of the following:
|
||||
* {@link #maxAttempts(int) maxAttempts}, {@link #delay(Duration) delay},
|
||||
* {@link #jitter(Duration) jitter}, {@link #multiplier(double) multiplier},
|
||||
* {@link #maxDelay(Duration) maxDelay}, or {@link #maxElapsedTime(Duration)
|
||||
* maxElapsedTime}.
|
||||
* @param backOff the {@code BackOff} strategy
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
*/
|
||||
public Builder backOff(BackOff backOff) {
|
||||
Assert.notNull(backOff, "BackOff must not be null");
|
||||
this.backOff = backOff;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of retry attempts.
|
||||
* <p>If a {@code maxAttempts} value has already been configured, the
|
||||
* supplied value will override the existing value.
|
||||
* <p>The default is {@value #DEFAULT_MAX_ATTEMPTS}.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p>You should not specify this configuration option if you have
|
||||
* configured a custom {@link #backOff(BackOff) BackOff} strategy.
|
||||
* @param maxAttempts the maximum number of retry attempts; must be
|
||||
* greater than zero
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
|
@ -112,15 +202,106 @@ public interface RetryPolicy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum retry {@link Duration}.
|
||||
* <p>If a {@code maxDuration} value has already been configured, the
|
||||
* supplied value will override the existing value.
|
||||
* @param maxDuration the maximum retry duration; must be positive
|
||||
* Specify the base delay after the initial invocation.
|
||||
* <p>If a {@linkplain #multiplier(double) multiplier} is specified, this
|
||||
* serves as the initial delay to multiply from.
|
||||
* <p>The default is {@value #DEFAULT_DELAY} milliseconds.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p>You should not specify this configuration option if you have
|
||||
* configured a custom {@link #backOff(BackOff) BackOff} strategy.
|
||||
* @param delay the base delay, typically in milliseconds or seconds;
|
||||
* must be positive
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #jitter(Duration)
|
||||
* @see #multiplier(double)
|
||||
* @see #maxDelay(Duration)
|
||||
*/
|
||||
public Builder delay(Duration delay) {
|
||||
assertIsPositive("delay", delay);
|
||||
this.delay = delay;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a jitter value for the base retry attempt, randomly subtracted
|
||||
* or added to the calculated delay, resulting in a value between
|
||||
* {@code delay - jitter} and {@code delay + jitter} but never below the
|
||||
* {@linkplain #delay(Duration) base delay} or above the
|
||||
* {@linkplain #maxDelay(Duration) max delay}.
|
||||
* <p>If a {@linkplain #multiplier(double) multiplier} is specified, it
|
||||
* is applied to the jitter value as well.
|
||||
* <p>The default is no jitter.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p>You should not specify this configuration option if you have
|
||||
* configured a custom {@link #backOff(BackOff) BackOff} strategy.
|
||||
* @param jitter the jitter value, typically in milliseconds; must be positive
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #delay(Duration)
|
||||
* @see #multiplier(double)
|
||||
* @see #maxDelay(Duration)
|
||||
*/
|
||||
public Builder jitter(Duration jitter) {
|
||||
Assert.isTrue(!jitter.isNegative(),
|
||||
() -> "Invalid jitter (%dms): must be >= 0.".formatted(jitter.toMillis()));
|
||||
this.jitter = jitter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a multiplier for a delay for the next retry attempt, applied
|
||||
* to the previous delay (starting with the initial
|
||||
* {@linkplain #delay(Duration) delay}) as well as to the applicable
|
||||
* {@linkplain #jitter(Duration) jitter} for each attempt.
|
||||
* <p>The default is {@value Builder#DEFAULT_MULTIPLIER}, effectively
|
||||
* resulting in a fixed delay.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p>You should not specify this configuration option if you have
|
||||
* configured a custom {@link #backOff(BackOff) BackOff} strategy.
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #delay(Duration)
|
||||
* @see #jitter(Duration)
|
||||
* @see #maxDelay(Duration)
|
||||
*/
|
||||
public Builder multiplier(double multiplier) {
|
||||
Assert.isTrue(multiplier >= 1, () -> "Invalid multiplier '" + multiplier + "': " +
|
||||
"must be greater than or equal to 1. A multiplier of 1 is equivalent to a fixed delay.");
|
||||
this.multiplier = multiplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum delay for any retry attempt, limiting how far
|
||||
* {@linkplain #jitter(Duration) jitter} and the
|
||||
* {@linkplain #multiplier(double) multiplier} can increase the
|
||||
* {@linkplain #delay(Duration) delay}.
|
||||
* <p>The default is unlimited.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p>You should not specify this configuration option if you have
|
||||
* configured a custom {@link #backOff(BackOff) BackOff} strategy.
|
||||
* @param maxDelay the maximum delay; must be positive
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #delay(Duration)
|
||||
* @see #jitter(Duration)
|
||||
* @see #multiplier(double)
|
||||
*/
|
||||
public Builder maxDelay(Duration maxDelay) {
|
||||
assertIsPositive("max delay", maxDelay);
|
||||
this.maxDelay = maxDelay;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum elapsed time.
|
||||
* <p>The default is unlimited.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p>You should not specify this configuration option if you have
|
||||
* configured a custom {@link #backOff(BackOff) BackOff} strategy.
|
||||
* @param maxElapsedTime the maximum elapsed time; 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;
|
||||
public Builder maxElapsedTime(Duration maxElapsedTime) {
|
||||
assertIsPositive("max elapsed time", maxElapsedTime);
|
||||
this.maxElapsedTime = maxElapsedTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -130,10 +311,14 @@ public interface RetryPolicy {
|
|||
* <p>Defaults to all exception types.
|
||||
* <p>If included exception types have already been configured, the supplied
|
||||
* types will be added to the existing list of included types.
|
||||
* <p>This can be combined with {@link #excludes(Class...)} and
|
||||
* {@link #predicate(Predicate)}.
|
||||
* <p>This can be combined with other {@code includes}, {@code excludes},
|
||||
* and a custom {@code predicate}.
|
||||
* @param types the types of exceptions to include in the policy
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #includes(Collection)
|
||||
* @see #excludes(Class...)
|
||||
* @see #excludes(Collection)
|
||||
* @see #predicate(Predicate)
|
||||
*/
|
||||
@SafeVarargs // Making the method final allows us to use @SafeVarargs.
|
||||
@SuppressWarnings("varargs")
|
||||
|
@ -142,15 +327,39 @@ public interface RetryPolicy {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the types of exceptions for which the {@link RetryPolicy}
|
||||
* should retry a failed operation.
|
||||
* <p>Defaults to all exception types.
|
||||
* <p>If included exception types have already been configured, the supplied
|
||||
* types will be added to the existing list of included types.
|
||||
* <p>This can be combined with other {@code includes}, {@code excludes},
|
||||
* and a custom {@code predicate}.
|
||||
* @param types the types of exceptions to include in the policy
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #includes(Class...)
|
||||
* @see #excludes(Class...)
|
||||
* @see #excludes(Collection)
|
||||
* @see #predicate(Predicate)
|
||||
*/
|
||||
public Builder includes(Collection<Class<? extends Throwable>> types) {
|
||||
this.includes.addAll(types);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the types of exceptions for which the {@link RetryPolicy}
|
||||
* should not retry a failed operation.
|
||||
* <p>If excluded exception types have already been configured, the supplied
|
||||
* types will be added to the existing list of excluded types.
|
||||
* <p>This can be combined with {@link #includes(Class...)} and
|
||||
* {@link #predicate(Predicate)}.
|
||||
* <p>This can be combined with {@code includes}, other {@code excludes},
|
||||
* and a custom {@code predicate}.
|
||||
* @param types the types of exceptions to exclude from the policy
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #includes(Class...)
|
||||
* @see #includes(Collection)
|
||||
* @see #excludes(Collection)
|
||||
* @see #predicate(Predicate)
|
||||
*/
|
||||
@SafeVarargs // Making the method final allows us to use @SafeVarargs.
|
||||
@SuppressWarnings("varargs")
|
||||
|
@ -159,6 +368,25 @@ public interface RetryPolicy {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the types of exceptions for which the {@link RetryPolicy}
|
||||
* should not retry a failed operation.
|
||||
* <p>If excluded exception types have already been configured, the supplied
|
||||
* types will be added to the existing list of excluded types.
|
||||
* <p>This can be combined with {@code includes}, other {@code excludes},
|
||||
* and a custom {@code predicate}.
|
||||
* @param types the types of exceptions to exclude from the policy
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #includes(Class...)
|
||||
* @see #includes(Collection)
|
||||
* @see #excludes(Class...)
|
||||
* @see #predicate(Predicate)
|
||||
*/
|
||||
public Builder excludes(Collection<Class<? extends Throwable>> types) {
|
||||
this.excludes.addAll(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
|
||||
|
@ -166,10 +394,13 @@ public interface RetryPolicy {
|
|||
* <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...)}.
|
||||
* <p>This can be combined with {@code includes} and {@code excludes}.
|
||||
* @param predicate a custom predicate
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @see #includes(Class...)
|
||||
* @see #includes(Collection)
|
||||
* @see #excludes(Class...)
|
||||
* @see #excludes(Collection)
|
||||
*/
|
||||
public Builder predicate(Predicate<Throwable> predicate) {
|
||||
this.predicate = (this.predicate != null ? this.predicate.and(predicate) : predicate);
|
||||
|
@ -177,11 +408,37 @@ public interface RetryPolicy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Build the {@link RetryPolicy} configured via this {@code Builder}.
|
||||
* Build the configured {@link RetryPolicy}.
|
||||
*/
|
||||
public RetryPolicy build() {
|
||||
return new DefaultRetryPolicy(this.maxAttempts, this.maxDuration,
|
||||
this.includes, this.excludes, this.predicate);
|
||||
BackOff backOff = this.backOff;
|
||||
if (backOff != null) {
|
||||
boolean misconfigured = (this.maxAttempts != 0) || (this.delay != null) || (this.jitter != null) ||
|
||||
(this.multiplier != 0) || (this.maxDelay != null) || (this.maxElapsedTime != null);
|
||||
Assert.state(!misconfigured, """
|
||||
The following configuration options are not supported with a custom BackOff strategy: \
|
||||
maxAttempts, delay, jitter, multiplier, maxDelay, or maxElapsedTime.""");
|
||||
}
|
||||
else {
|
||||
ExponentialBackOff exponentialBackOff = new ExponentialBackOff();
|
||||
exponentialBackOff.setMaxAttempts(this.maxAttempts > 0 ? this.maxAttempts : DEFAULT_MAX_ATTEMPTS);
|
||||
exponentialBackOff.setInitialInterval(this.delay != null ? this.delay.toMillis() : DEFAULT_DELAY);
|
||||
exponentialBackOff.setMaxInterval(this.maxDelay != null ? this.maxDelay.toMillis() : DEFAULT_MAX_DELAY);
|
||||
exponentialBackOff.setMultiplier(this.multiplier > 1 ? this.multiplier : DEFAULT_MULTIPLIER);
|
||||
if (this.jitter != null) {
|
||||
exponentialBackOff.setJitter(this.jitter.toMillis());
|
||||
}
|
||||
if (this.maxElapsedTime != null) {
|
||||
exponentialBackOff.setMaxElapsedTime(this.maxElapsedTime.toMillis());
|
||||
}
|
||||
backOff = exponentialBackOff;
|
||||
}
|
||||
return new DefaultRetryPolicy(this.includes, this.excludes, this.predicate, backOff);
|
||||
}
|
||||
|
||||
private static void assertIsPositive(String name, Duration duration) {
|
||||
Assert.isTrue((!duration.isNegative() && !duration.isZero()),
|
||||
() -> "Invalid duration (%dms): %s must be positive.".formatted(duration.toMillis(), name));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,18 +26,16 @@ import org.springframework.core.log.LogAccessor;
|
|||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
import org.springframework.util.backoff.BackOffExecution;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
/**
|
||||
* A basic implementation of {@link RetryOperations} that executes and potentially
|
||||
* retries a {@link Retryable} operation based on a configured {@link RetryPolicy}
|
||||
* and {@link BackOff} policy.
|
||||
* retries a {@link Retryable} operation based on a configured {@link RetryPolicy}.
|
||||
*
|
||||
* <p>By default, a retryable operation will be retried at most 3 times with a
|
||||
* fixed backoff of 1 second.
|
||||
*
|
||||
* <p>A {@link RetryListener} can be {@linkplain #setRetryListener(RetryListener)
|
||||
* registered} to intercept and inject behavior during key retry phases (before a
|
||||
* registered} to react to events published during key retry phases (before a
|
||||
* retry attempt, after a retry attempt, etc.).
|
||||
*
|
||||
* <p>All retry actions performed by this template are logged at debug level, using
|
||||
|
@ -58,9 +56,7 @@ public class RetryTemplate implements RetryOperations {
|
|||
private static final LogAccessor logger = new LogAccessor(RetryTemplate.class);
|
||||
|
||||
|
||||
private RetryPolicy retryPolicy = RetryPolicy.withMaxAttempts(3);
|
||||
|
||||
private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
|
||||
private RetryPolicy retryPolicy = RetryPolicy.withDefaults();
|
||||
|
||||
private RetryListener retryListener = new RetryListener() {};
|
||||
|
||||
|
@ -68,13 +64,13 @@ public class RetryTemplate implements RetryOperations {
|
|||
/**
|
||||
* Create a new {@code RetryTemplate} with maximum 3 retry attempts and a
|
||||
* fixed backoff of 1 second.
|
||||
* @see RetryPolicy#withDefaults()
|
||||
*/
|
||||
public RetryTemplate() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and a
|
||||
* fixed backoff of 1 second.
|
||||
* Create a new {@code RetryTemplate} with the supplied {@link RetryPolicy}.
|
||||
* @param retryPolicy the retry policy to use
|
||||
*/
|
||||
public RetryTemplate(RetryPolicy retryPolicy) {
|
||||
|
@ -82,25 +78,14 @@ public class RetryTemplate implements RetryOperations {
|
|||
this.retryPolicy = retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and
|
||||
* {@link BackOff} policy.
|
||||
* @param retryPolicy the retry policy to use
|
||||
* @param backOffPolicy the backoff policy to use
|
||||
*/
|
||||
public RetryTemplate(RetryPolicy retryPolicy, BackOff backOffPolicy) {
|
||||
this(retryPolicy);
|
||||
Assert.notNull(backOffPolicy, "BackOff policy must not be null");
|
||||
this.backOffPolicy = backOffPolicy;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@link RetryPolicy} to use.
|
||||
* <p>Defaults to {@code RetryPolicy.withMaxAttempts(3)}.
|
||||
* <p>Defaults to {@code RetryPolicy.withDefaults()}.
|
||||
* @param retryPolicy the retry policy to use
|
||||
* @see RetryPolicy#withDefaults()
|
||||
* @see RetryPolicy#withMaxAttempts(int)
|
||||
* @see RetryPolicy#withMaxDuration(Duration)
|
||||
* @see RetryPolicy#withMaxElapsedTime(Duration)
|
||||
* @see RetryPolicy#builder()
|
||||
*/
|
||||
public void setRetryPolicy(RetryPolicy retryPolicy) {
|
||||
|
@ -108,17 +93,6 @@ public class RetryTemplate implements RetryOperations {
|
|||
this.retryPolicy = retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link BackOff} policy to use.
|
||||
* <p>Defaults to {@code new FixedBackOff(Duration.ofSeconds(1))}.
|
||||
* @param backOffPolicy the backoff policy to use
|
||||
* @see FixedBackOff
|
||||
*/
|
||||
public void setBackOffPolicy(BackOff backOffPolicy) {
|
||||
Assert.notNull(backOffPolicy, "BackOff policy must not be null");
|
||||
this.backOffPolicy = backOffPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link RetryListener} to use.
|
||||
* <p>If multiple listeners are needed, use a
|
||||
|
@ -157,13 +131,12 @@ public class RetryTemplate implements RetryOperations {
|
|||
() -> "Execution of retryable operation '%s' failed; initiating the retry process"
|
||||
.formatted(retryableName));
|
||||
// Retry process starts here
|
||||
RetryExecution retryExecution = this.retryPolicy.start();
|
||||
BackOffExecution backOffExecution = this.backOffPolicy.start();
|
||||
BackOffExecution backOffExecution = this.retryPolicy.getBackOff().start();
|
||||
Deque<Throwable> exceptions = new ArrayDeque<>();
|
||||
exceptions.add(initialException);
|
||||
|
||||
Throwable retryException = initialException;
|
||||
while (retryExecution.shouldRetry(retryException)) {
|
||||
while (this.retryPolicy.shouldRetry(retryException)) {
|
||||
try {
|
||||
long duration = backOffExecution.nextBackOff();
|
||||
if (duration == BackOffExecution.STOP) {
|
||||
|
@ -181,9 +154,9 @@ public class RetryTemplate implements RetryOperations {
|
|||
}
|
||||
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName));
|
||||
try {
|
||||
this.retryListener.beforeRetry(retryExecution);
|
||||
this.retryListener.beforeRetry(this.retryPolicy, retryable);
|
||||
R result = retryable.execute();
|
||||
this.retryListener.onRetrySuccess(retryExecution, result);
|
||||
this.retryListener.onRetrySuccess(this.retryPolicy, retryable, result);
|
||||
logger.debug(() -> "Retryable operation '%s' completed successfully after retry"
|
||||
.formatted(retryableName));
|
||||
return result;
|
||||
|
@ -191,7 +164,7 @@ public class RetryTemplate implements RetryOperations {
|
|||
catch (Throwable currentAttemptException) {
|
||||
logger.debug(() -> "Retry attempt for operation '%s' failed due to '%s'"
|
||||
.formatted(retryableName, currentAttemptException));
|
||||
this.retryListener.onRetryFailure(retryExecution, currentAttemptException);
|
||||
this.retryListener.onRetryFailure(this.retryPolicy, retryable, currentAttemptException);
|
||||
exceptions.add(currentAttemptException);
|
||||
retryException = currentAttemptException;
|
||||
}
|
||||
|
@ -203,7 +176,7 @@ public class RetryTemplate implements RetryOperations {
|
|||
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
|
||||
exceptions.removeLast());
|
||||
exceptions.forEach(finalException::addSuppressed);
|
||||
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException);
|
||||
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, finalException);
|
||||
throw finalException;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,10 @@ import java.util.List;
|
|||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.retry.RetryExecution;
|
||||
import org.springframework.core.retry.RetryListener;
|
||||
import org.springframework.core.retry.RetryPolicy;
|
||||
import org.springframework.core.retry.RetryTemplate;
|
||||
import org.springframework.core.retry.Retryable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
@ -66,23 +67,23 @@ public class CompositeRetryListener implements RetryListener {
|
|||
|
||||
|
||||
@Override
|
||||
public void beforeRetry(RetryExecution retryExecution) {
|
||||
this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryExecution));
|
||||
public void beforeRetry(RetryPolicy retryPolicy, Retryable<?> retryable) {
|
||||
this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryPolicy, retryable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
|
||||
this.listeners.forEach(listener -> listener.onRetrySuccess(retryExecution, result));
|
||||
public void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, @Nullable Object result) {
|
||||
this.listeners.forEach(listener -> listener.onRetrySuccess(retryPolicy, retryable, result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetryFailure(RetryExecution retryExecution, Throwable throwable) {
|
||||
this.listeners.forEach(listener -> listener.onRetryFailure(retryExecution, throwable));
|
||||
public void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
|
||||
this.listeners.forEach(listener -> listener.onRetryFailure(retryPolicy, retryable, throwable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) {
|
||||
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryExecution, throwable));
|
||||
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
|
||||
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,188 +0,0 @@
|
|||
/*
|
||||
* Copyright 2002-present 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,125 @@
|
|||
/*
|
||||
* Copyright 2002-present 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 java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.util.backoff.BackOffExecution.STOP;
|
||||
|
||||
/**
|
||||
* Max attempts {@link RetryPolicy} tests.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
*/
|
||||
class MaxAttemptsRetryPolicyTests {
|
||||
|
||||
@Test
|
||||
void maxAttempts() {
|
||||
var retryPolicy = RetryPolicy.builder().maxAttempts(2).delay(Duration.ofMillis(1)).build();
|
||||
var backOffExecution = retryPolicy.getBackOff().start();
|
||||
var throwable = mock(Throwable.class);
|
||||
|
||||
assertThat(retryPolicy.shouldRetry(throwable)).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(throwable)).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
|
||||
assertThat(retryPolicy.shouldRetry(throwable)).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isEqualTo(STOP);
|
||||
assertThat(retryPolicy.shouldRetry(throwable)).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isEqualTo(STOP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxAttemptsAndPredicate() {
|
||||
var retryPolicy = RetryPolicy.builder()
|
||||
.maxAttempts(4)
|
||||
.delay(Duration.ofMillis(1))
|
||||
.predicate(NumberFormatException.class::isInstance)
|
||||
.build();
|
||||
|
||||
var backOffExecution = retryPolicy.getBackOff().start();
|
||||
|
||||
// 4 retries
|
||||
assertThat(retryPolicy.shouldRetry(new NumberFormatException())).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new IllegalStateException())).isFalse();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new IllegalStateException())).isFalse();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new CustomNumberFormatException())).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
|
||||
// After policy exhaustion
|
||||
assertThat(retryPolicy.shouldRetry(new NumberFormatException())).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isEqualTo(STOP);
|
||||
assertThat(retryPolicy.shouldRetry(new IllegalStateException())).isFalse();
|
||||
assertThat(backOffExecution.nextBackOff()).isEqualTo(STOP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxAttemptsWithIncludesAndExcludes() {
|
||||
var retryPolicy = RetryPolicy.builder()
|
||||
.maxAttempts(6)
|
||||
.includes(RuntimeException.class, IOException.class)
|
||||
.excludes(FileNotFoundException.class, CustomFileSystemException.class)
|
||||
.build();
|
||||
|
||||
var backOffExecution = retryPolicy.getBackOff().start();
|
||||
|
||||
// 6 retries
|
||||
assertThat(retryPolicy.shouldRetry(new IOException())).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new RuntimeException())).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new FileNotFoundException())).isFalse();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new FileSystemException("file"))).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new CustomFileSystemException("file"))).isFalse();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
assertThat(retryPolicy.shouldRetry(new IOException())).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isGreaterThan(0);
|
||||
|
||||
// After policy exhaustion
|
||||
assertThat(retryPolicy.shouldRetry(new IOException())).isTrue();
|
||||
assertThat(backOffExecution.nextBackOff()).isEqualTo(STOP);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class CustomNumberFormatException extends NumberFormatException {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class CustomFileSystemException extends FileSystemException {
|
||||
|
||||
CustomFileSystemException(String file) {
|
||||
super(file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright 2002-present 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,455 @@
|
|||
/*
|
||||
* Copyright 2002-present 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 java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.backoff.ExponentialBackOff;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link RetryPolicy} and its builder.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @see RetryTemplateTests
|
||||
*/
|
||||
class RetryPolicyTests {
|
||||
|
||||
@Nested
|
||||
class FactoryMethodTests {
|
||||
|
||||
@Test
|
||||
void withDefaults() {
|
||||
var policy = RetryPolicy.withDefaults();
|
||||
|
||||
assertThat(policy.shouldRetry(new AssertionError())).isTrue();
|
||||
assertThat(policy.shouldRetry(new IOException())).isTrue();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(FixedBackOff.class))
|
||||
.satisfies(backOff -> {
|
||||
assertThat(backOff.getMaxAttempts()).isEqualTo(3);
|
||||
assertThat(backOff.getInterval()).isEqualTo(1000);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void withMaxAttemptsPreconditions() {
|
||||
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 withMaxAttempts() {
|
||||
var policy = RetryPolicy.withMaxAttempts(5);
|
||||
|
||||
assertThat(policy.shouldRetry(new AssertionError())).isTrue();
|
||||
assertThat(policy.shouldRetry(new IOException())).isTrue();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(FixedBackOff.class))
|
||||
.satisfies(backOff -> {
|
||||
assertThat(backOff.getMaxAttempts()).isEqualTo(5);
|
||||
assertThat(backOff.getInterval()).isEqualTo(1000);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void withMaxElapsedTimePreconditions() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.withMaxElapsedTime(Duration.ofMillis(0)))
|
||||
.withMessage("Invalid duration (0ms): max elapsed time must be positive.");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.withMaxElapsedTime(Duration.ofMillis(-1)))
|
||||
.withMessage("Invalid duration (-1ms): max elapsed time must be positive.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withMaxElapsedTime() {
|
||||
var policy = RetryPolicy.withMaxElapsedTime(Duration.ofMillis(42));
|
||||
|
||||
assertThat(policy.shouldRetry(new AssertionError())).isTrue();
|
||||
assertThat(policy.shouldRetry(new IOException())).isTrue();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay())
|
||||
.extracting(ExponentialBackOff::getMaxElapsedTime).isEqualTo(42L);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class BuilderTests {
|
||||
|
||||
@Test
|
||||
void backOffPlusConflictingConfig() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().backOff(mock()).delay(Duration.ofMillis(10)).build())
|
||||
.withMessage("""
|
||||
The following configuration options are not supported with a custom BackOff strategy: \
|
||||
maxAttempts, delay, jitter, multiplier, maxDelay, or maxElapsedTime.""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void backOff() {
|
||||
var backOff = new FixedBackOff();
|
||||
var policy = RetryPolicy.builder().backOff(backOff).build();
|
||||
|
||||
assertThat(policy.getBackOff()).isEqualTo(backOff);
|
||||
|
||||
assertThat(policy).asString()
|
||||
.isEqualTo("DefaultRetryPolicy[backOff=FixedBackOff[interval=5000, maxAttempts=unlimited]]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxAttemptsPreconditions() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().maxAttempts(0))
|
||||
.withMessage("Max attempts must be greater than zero");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().maxAttempts(-1))
|
||||
.withMessage("Max attempts must be greater than zero");
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxAttempts() {
|
||||
var policy = RetryPolicy.builder().maxAttempts(5).build();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(backOff -> {
|
||||
assertThat(backOff.getMaxAttempts()).isEqualTo(5);
|
||||
assertThat(backOff.getInitialInterval()).isEqualTo(1000);
|
||||
});
|
||||
|
||||
assertToString(policy, 1000, 0, 1, Long.MAX_VALUE, Long.MAX_VALUE, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void delayPreconditions() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().delay(Duration.ofMillis(0)))
|
||||
.withMessage("Invalid duration (0ms): delay must be positive.");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().delay(Duration.ofMillis(-1)))
|
||||
.withMessage("Invalid duration (-1ms): delay must be positive.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void delay() {
|
||||
var policy = RetryPolicy.builder().delay(Duration.ofMillis(42)).build();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(backOff -> {
|
||||
assertThat(backOff.getInitialInterval()).isEqualTo(42);
|
||||
assertThat(backOff.getMaxAttempts()).isEqualTo(3);
|
||||
});
|
||||
|
||||
assertToString(policy, 42, 0, 1, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jitterPreconditions() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().jitter(Duration.ofMillis(-1)))
|
||||
.withMessage("Invalid jitter (-1ms): must be >= 0.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jitter() {
|
||||
var policy = RetryPolicy.builder().jitter(Duration.ofMillis(42)).build();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay())
|
||||
.extracting(ExponentialBackOff::getJitter).isEqualTo(42L);
|
||||
|
||||
assertToString(policy, 1000, 42, 1, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiplierPreconditions() {
|
||||
String template = """
|
||||
Invalid multiplier '%s': must be greater than or equal to 1. \
|
||||
A multiplier of 1 is equivalent to a fixed delay.""";
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().multiplier(-1))
|
||||
.withMessage(template, "-1.0");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().multiplier(0))
|
||||
.withMessage(template, "0.0");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().multiplier(0.5))
|
||||
.withMessage(template, "0.5");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiplier() {
|
||||
var policy = RetryPolicy.builder().multiplier(1.5).build();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay())
|
||||
.extracting(ExponentialBackOff::getMultiplier).isEqualTo(1.5);
|
||||
|
||||
assertToString(policy, 1000, 0, 1.5, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxDelayPreconditions() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().maxDelay(Duration.ofMillis(0)))
|
||||
.withMessage("Invalid duration (0ms): max delay must be positive.");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().maxDelay(Duration.ofMillis(-1)))
|
||||
.withMessage("Invalid duration (-1ms): max delay must be positive.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxDelay() {
|
||||
var policy = RetryPolicy.builder().maxDelay(Duration.ofMillis(42)).build();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay())
|
||||
.extracting(ExponentialBackOff::getMaxInterval).isEqualTo(42L);
|
||||
|
||||
assertToString(policy, 1000, 0, 1, 42, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxElapsedTimePreconditions() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().maxElapsedTime(Duration.ofMillis(0)))
|
||||
.withMessage("Invalid duration (0ms): max elapsed time must be positive.");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RetryPolicy.builder().maxElapsedTime(Duration.ofMillis(-1)))
|
||||
.withMessage("Invalid duration (-1ms): max elapsed time must be positive.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxElapsedTime() {
|
||||
var policy = RetryPolicy.builder().maxElapsedTime(Duration.ofMillis(42)).build();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay())
|
||||
.extracting(ExponentialBackOff::getMaxElapsedTime).isEqualTo(42L);
|
||||
|
||||
assertToString(policy, 1000, 0, 1, Long.MAX_VALUE, 42, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void includes() {
|
||||
var policy = RetryPolicy.builder()
|
||||
.includes(FileNotFoundException.class, IllegalArgumentException.class)
|
||||
.includes(List.of(NumberFormatException.class, AssertionError.class))
|
||||
.build();
|
||||
|
||||
assertThat(policy.shouldRetry(new FileNotFoundException())).isTrue();
|
||||
assertThat(policy.shouldRetry(new IllegalArgumentException())).isTrue();
|
||||
assertThat(policy.shouldRetry(new NumberFormatException())).isTrue();
|
||||
assertThat(policy.shouldRetry(new AssertionError())).isTrue();
|
||||
|
||||
assertThat(policy.shouldRetry(new Throwable())).isFalse();
|
||||
assertThat(policy.shouldRetry(new FileSystemException("fs"))).isFalse();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay());
|
||||
|
||||
String filters = "includes=" + names(FileNotFoundException.class, IllegalArgumentException.class,
|
||||
NumberFormatException.class, AssertionError.class) + ", ";
|
||||
assertToString(policy, filters, 1000, 0, 1, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void includesSubtypeMatching() {
|
||||
var policy = RetryPolicy.builder().includes(IOException.class).build();
|
||||
|
||||
assertThat(policy.shouldRetry(new FileNotFoundException())).isTrue();
|
||||
assertThat(policy.shouldRetry(new FileSystemException("fs"))).isTrue();
|
||||
|
||||
assertThat(policy.shouldRetry(new Throwable())).isFalse();
|
||||
assertThat(policy.shouldRetry(new AssertionError())).isFalse();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay());
|
||||
|
||||
assertToString(policy, "includes=[java.io.IOException], ", 1000, 0, 1, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludes() {
|
||||
var policy = RetryPolicy.builder()
|
||||
.excludes(FileNotFoundException.class, IllegalArgumentException.class)
|
||||
.excludes(List.of(NumberFormatException.class, AssertionError.class))
|
||||
.build();
|
||||
|
||||
assertThat(policy.shouldRetry(new FileNotFoundException())).isFalse();
|
||||
assertThat(policy.shouldRetry(new IllegalArgumentException())).isFalse();
|
||||
assertThat(policy.shouldRetry(new NumberFormatException())).isFalse();
|
||||
assertThat(policy.shouldRetry(new AssertionError())).isFalse();
|
||||
|
||||
assertThat(policy.shouldRetry(new Throwable())).isTrue();
|
||||
assertThat(policy.shouldRetry(new FileSystemException("fs"))).isTrue();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay());
|
||||
|
||||
String filters = "excludes=" + names(FileNotFoundException.class, IllegalArgumentException.class,
|
||||
NumberFormatException.class, AssertionError.class) + ", ";
|
||||
assertToString(policy, filters, 1000, 0, 1, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludesSubtypeMatching() {
|
||||
var policy = RetryPolicy.builder().excludes(IOException.class).build();
|
||||
|
||||
assertThat(policy.shouldRetry(new IOException("fs"))).isFalse();
|
||||
assertThat(policy.shouldRetry(new FileNotFoundException())).isFalse();
|
||||
assertThat(policy.shouldRetry(new FileSystemException("fs"))).isFalse();
|
||||
|
||||
assertThat(policy.shouldRetry(new Throwable())).isTrue();
|
||||
assertThat(policy.shouldRetry(new AssertionError())).isTrue();
|
||||
|
||||
assertToString(policy, "excludes=[java.io.IOException], ", 1000, 0, 1, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void predicate() {
|
||||
var policy = RetryPolicy.builder()
|
||||
.predicate(new NumberFormatExceptionMatcher())
|
||||
.build();
|
||||
|
||||
assertThat(policy.shouldRetry(new NumberFormatException())).isTrue();
|
||||
assertThat(policy.shouldRetry(new CustomNumberFormatException())).isTrue();
|
||||
|
||||
assertThat(policy.shouldRetry(new Throwable())).isFalse();
|
||||
assertThat(policy.shouldRetry(new Exception())).isFalse();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay());
|
||||
|
||||
assertToString(policy, "predicate=NumberFormatExceptionMatcher, ",
|
||||
1000, 0, 1, Long.MAX_VALUE, Long.MAX_VALUE, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void predicatesCombined() {
|
||||
var BOOM = "Boom!";
|
||||
var policy = RetryPolicy.builder()
|
||||
.predicate(new NumberFormatExceptionMatcher())
|
||||
.predicate(throwable -> BOOM.equals(throwable.getMessage()))
|
||||
.build();
|
||||
|
||||
assertThat(policy.shouldRetry(new NumberFormatException(BOOM))).isTrue();
|
||||
assertThat(policy.shouldRetry(new CustomNumberFormatException(BOOM))).isTrue();
|
||||
|
||||
assertThat(policy.shouldRetry(new NumberFormatException())).isFalse();
|
||||
assertThat(policy.shouldRetry(new CustomNumberFormatException())).isFalse();
|
||||
assertThat(policy.shouldRetry(new Throwable())).isFalse();
|
||||
assertThat(policy.shouldRetry(new Exception())).isFalse();
|
||||
|
||||
assertThat(policy.getBackOff())
|
||||
.asInstanceOf(type(ExponentialBackOff.class))
|
||||
.satisfies(hasDefaultMaxAttemptsAndDelay());
|
||||
|
||||
assertThat(policy).asString()
|
||||
.matches("DefaultRetryPolicy\\[predicate=Predicate.+?Lambda.+?, backOff=ExponentialBackOff\\[.+?]]");
|
||||
}
|
||||
|
||||
private static void assertToString(RetryPolicy policy, long initialInterval, long jitter,
|
||||
double multiplier, long maxInterval, long maxElapsedTime, int maxAttempts) {
|
||||
|
||||
assertToString(policy, "", initialInterval, jitter, multiplier, maxInterval, maxElapsedTime, maxAttempts);
|
||||
}
|
||||
|
||||
private static void assertToString(RetryPolicy policy, String filters, long initialInterval, long jitter,
|
||||
double multiplier, long maxInterval, long maxElapsedTime, int maxAttempts) {
|
||||
|
||||
assertThat(policy).asString()
|
||||
.isEqualTo("""
|
||||
DefaultRetryPolicy[%sbackOff=ExponentialBackOff[\
|
||||
initialInterval=%d, \
|
||||
jitter=%d, \
|
||||
multiplier=%s, \
|
||||
maxInterval=%d, \
|
||||
maxElapsedTime=%d, \
|
||||
maxAttempts=%d\
|
||||
]]""",
|
||||
filters, initialInterval, jitter, multiplier, maxInterval, maxElapsedTime, maxAttempts);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
@SuppressWarnings("unchecked")
|
||||
private static String names(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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static ThrowingConsumer<? super ExponentialBackOff> hasDefaultMaxAttemptsAndDelay() {
|
||||
return backOff -> {
|
||||
assertThat(backOff.getMaxAttempts()).isEqualTo(3);
|
||||
assertThat(backOff.getInitialInterval()).isEqualTo(1000);
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class CustomNumberFormatException extends NumberFormatException {
|
||||
|
||||
CustomNumberFormatException() {
|
||||
}
|
||||
|
||||
CustomNumberFormatException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -30,18 +30,17 @@ import org.junit.jupiter.params.ParameterizedTest;
|
|||
import org.junit.jupiter.params.provider.Arguments.ArgumentSet;
|
||||
import org.junit.jupiter.params.provider.FieldSource;
|
||||
|
||||
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.junit.jupiter.params.provider.Arguments.argumentSet;
|
||||
|
||||
/**
|
||||
* Tests for {@link RetryTemplate}.
|
||||
* Integration tests for {@link RetryTemplate} and {@link RetryPolicy}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @see RetryPolicyTests
|
||||
*/
|
||||
class RetryTemplateTests {
|
||||
|
||||
|
@ -49,8 +48,13 @@ class RetryTemplateTests {
|
|||
|
||||
|
||||
@BeforeEach
|
||||
void configureTemplate() {
|
||||
this.retryTemplate.setBackOffPolicy(new FixedBackOff(Duration.ofMillis(10)));
|
||||
void configureRetryTemplate() {
|
||||
var retryPolicy = RetryPolicy.builder()
|
||||
.maxAttempts(3)
|
||||
.delay(Duration.ofMillis(1))
|
||||
.build();
|
||||
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -109,7 +113,7 @@ class RetryTemplateTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void retryWithFailingRetryableAndCustomRetryPolicyWithMultiplePredicates() {
|
||||
void retryWithFailingRetryableAndMultiplePredicates() {
|
||||
var invocationCount = new AtomicInteger();
|
||||
var exception = new NumberFormatException("Boom!");
|
||||
|
||||
|
@ -128,7 +132,7 @@ class RetryTemplateTests {
|
|||
|
||||
var retryPolicy = RetryPolicy.builder()
|
||||
.maxAttempts(5)
|
||||
.maxDuration(Duration.ofMillis(100))
|
||||
.delay(Duration.ofMillis(1))
|
||||
.predicate(NumberFormatException.class::isInstance)
|
||||
.predicate(t -> t.getMessage().equals("Boom!"))
|
||||
.build();
|
||||
|
@ -167,6 +171,7 @@ class RetryTemplateTests {
|
|||
|
||||
var retryPolicy = RetryPolicy.builder()
|
||||
.maxAttempts(Integer.MAX_VALUE)
|
||||
.delay(Duration.ofMillis(1))
|
||||
.includes(IOException.class)
|
||||
.build();
|
||||
|
||||
|
@ -189,11 +194,13 @@ class RetryTemplateTests {
|
|||
argumentSet("Excludes",
|
||||
RetryPolicy.builder()
|
||||
.maxAttempts(Integer.MAX_VALUE)
|
||||
.delay(Duration.ofMillis(1))
|
||||
.excludes(FileNotFoundException.class)
|
||||
.build()),
|
||||
argumentSet("Includes & Excludes",
|
||||
RetryPolicy.builder()
|
||||
.maxAttempts(Integer.MAX_VALUE)
|
||||
.delay(Duration.ofMillis(1))
|
||||
.includes(IOException.class)
|
||||
.excludes(FileNotFoundException.class)
|
||||
.build())
|
||||
|
@ -201,7 +208,7 @@ class RetryTemplateTests {
|
|||
|
||||
@ParameterizedTest
|
||||
@FieldSource("includesAndExcludesRetryPolicies")
|
||||
void retryWithIncludesAndExcludesRetryPolicies(RetryPolicy retryPolicy) {
|
||||
void retryWithExceptionIncludesAndExcludes(RetryPolicy retryPolicy) {
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
|
||||
var invocationCount = new AtomicInteger();
|
||||
|
|
|
@ -21,8 +21,9 @@ import java.util.List;
|
|||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.retry.RetryExecution;
|
||||
import org.springframework.core.retry.RetryListener;
|
||||
import org.springframework.core.retry.RetryPolicy;
|
||||
import org.springframework.core.retry.Retryable;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
@ -39,7 +40,8 @@ class CompositeRetryListenerTests {
|
|||
private final RetryListener listener1 = mock();
|
||||
private final RetryListener listener2 = mock();
|
||||
private final RetryListener listener3 = mock();
|
||||
private final RetryExecution retryExecution = mock();
|
||||
private final RetryPolicy retryPolicy = mock();
|
||||
private final Retryable<?> retryable = mock();
|
||||
|
||||
private final CompositeRetryListener compositeRetryListener =
|
||||
new CompositeRetryListener(List.of(listener1, listener2));
|
||||
|
@ -52,41 +54,41 @@ class CompositeRetryListenerTests {
|
|||
|
||||
@Test
|
||||
void beforeRetry() {
|
||||
compositeRetryListener.beforeRetry(retryExecution);
|
||||
compositeRetryListener.beforeRetry(retryPolicy, retryable);
|
||||
|
||||
verify(listener1).beforeRetry(retryExecution);
|
||||
verify(listener2).beforeRetry(retryExecution);
|
||||
verify(listener3).beforeRetry(retryExecution);
|
||||
verify(listener1).beforeRetry(retryPolicy, retryable);
|
||||
verify(listener2).beforeRetry(retryPolicy, retryable);
|
||||
verify(listener3).beforeRetry(retryPolicy, retryable);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onRetrySuccess() {
|
||||
Object result = new Object();
|
||||
compositeRetryListener.onRetrySuccess(retryExecution, result);
|
||||
compositeRetryListener.onRetrySuccess(retryPolicy, retryable, result);
|
||||
|
||||
verify(listener1).onRetrySuccess(retryExecution, result);
|
||||
verify(listener2).onRetrySuccess(retryExecution, result);
|
||||
verify(listener3).onRetrySuccess(retryExecution, result);
|
||||
verify(listener1).onRetrySuccess(retryPolicy, retryable, result);
|
||||
verify(listener2).onRetrySuccess(retryPolicy, retryable, result);
|
||||
verify(listener3).onRetrySuccess(retryPolicy, retryable, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onRetryFailure() {
|
||||
Exception exception = new Exception();
|
||||
compositeRetryListener.onRetryFailure(retryExecution, exception);
|
||||
compositeRetryListener.onRetryFailure(retryPolicy, retryable, exception);
|
||||
|
||||
verify(listener1).onRetryFailure(retryExecution, exception);
|
||||
verify(listener2).onRetryFailure(retryExecution, exception);
|
||||
verify(listener3).onRetryFailure(retryExecution, exception);
|
||||
verify(listener1).onRetryFailure(retryPolicy, retryable, exception);
|
||||
verify(listener2).onRetryFailure(retryPolicy, retryable, exception);
|
||||
verify(listener3).onRetryFailure(retryPolicy, retryable, exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onRetryPolicyExhaustion() {
|
||||
Exception exception = new Exception();
|
||||
compositeRetryListener.onRetryPolicyExhaustion(retryExecution, exception);
|
||||
compositeRetryListener.onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
|
||||
verify(listener1).onRetryPolicyExhaustion(retryExecution, exception);
|
||||
verify(listener2).onRetryPolicyExhaustion(retryExecution, exception);
|
||||
verify(listener3).onRetryPolicyExhaustion(retryExecution, exception);
|
||||
verify(listener1).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
verify(listener2).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
verify(listener3).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue