Browse Source

Match against exception causes in @⁠Retryable and RetryPolicy

Prior to this commit, our @⁠Retryable support as well as a RetryPolicy
created by the RetryPolicy.Builder only matched against top-level
exceptions when filtering included/excluded exceptions thrown by a
@⁠Retryable method or Retryable operation.

With this commit, we now match against not only top-level exceptions
but also nested causes within those top-level exceptions. This is
achieved via the new ExceptionTypeFilter.match(Throwable, boolean)
support.

See gh-35592
Closes gh-35583
pull/35603/head
Sam Brannen 4 months ago
parent
commit
97ae5fde7c
  1. 11
      framework-docs/modules/ROOT/pages/core/resilience.adoc
  2. 7
      spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java
  3. 2
      spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java
  4. 50
      spring-context/src/test/java/org/springframework/resilience/ReactiveRetryInterceptorTests.java
  5. 31
      spring-context/src/test/java/org/springframework/resilience/RejectMalformedInputException3Predicate.java
  6. 56
      spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java
  7. 2
      spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java
  8. 12
      spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java
  9. 13
      spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java

11
framework-docs/modules/ROOT/pages/core/resilience.adoc

@ -26,8 +26,10 @@ public void sendNotification() {
By default, the method invocation will be retried for any exception thrown: with at most 3 By default, the method invocation will be retried for any exception thrown: with at most 3
retry attempts after an initial failure, and a delay of 1 second between attempts. retry attempts after an initial failure, and a delay of 1 second between attempts.
This can be specifically adapted for every method if necessary – for example, by narrowing This can be specifically adapted for every method if necessary — for example, by narrowing
the exceptions to retry: the exceptions to retry via the `includes` and `excludes` attributes. The supplied
exception types will be matched against an exception thrown by a failed invocation as well
as nested causes.
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
@ -182,7 +184,8 @@ If you only need to customize the number of retry attempts, you can use the
<1> Explicitly uses `RetryPolicy.withMaxAttempts(5)`. <1> Explicitly uses `RetryPolicy.withMaxAttempts(5)`.
If you need to narrow the types of exceptions to retry, that can be achieved via the If you need to narrow the types of exceptions to retry, that can be achieved via the
`includes()` and `excludes()` builder methods. `includes()` and `excludes()` builder methods. The supplied exception types will be
matched against an exception thrown by a failed operation as well as nested causes.
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
@ -204,7 +207,7 @@ If you need to narrow the types of exceptions to retry, that can be achieved via
For advanced use cases, you can specify a custom `Predicate<Throwable>` via the For advanced use cases, you can specify a custom `Predicate<Throwable>` via the
`predicate()` method in the `RetryPolicy.Builder`, and the predicate will be used to `predicate()` method in the `RetryPolicy.Builder`, and the predicate will be used to
determine whether to retry a failed operation based on a given `Throwable` – for example, determine whether to retry a failed operation based on a given `Throwable` – for example,
by checking the cause or the message of the `Throwable`. by checking the message of the `Throwable`.
Custom predicates can be combined with `includes` and `excludes`; however, custom Custom predicates can be combined with `includes` and `excludes`; however, custom
predicates will always be applied after `includes` and `excludes` have been applied. predicates will always be applied after `includes` and `excludes` have been applied.

7
spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java

@ -40,6 +40,7 @@ import org.springframework.resilience.retry.MethodRetryPredicate;
* project but redesigned as a minimal core retry feature in the Spring Framework. * project but redesigned as a minimal core retry feature in the Spring Framework.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 7.0 * @since 7.0
* @see EnableResilientMethods * @see EnableResilientMethods
* @see RetryAnnotationBeanPostProcessor * @see RetryAnnotationBeanPostProcessor
@ -64,6 +65,9 @@ public @interface Retryable {
/** /**
* Applicable exception types to attempt a retry for. This attribute * Applicable exception types to attempt a retry for. This attribute
* allows for the convenient specification of assignable exception types. * allows for the convenient specification of assignable exception types.
* <p>The supplied exception types will be matched against an exception
* thrown by a failed invocation as well as nested
* {@linkplain Throwable#getCause() causes}.
* <p>This can optionally be combined with {@link #excludes() excludes} or * <p>This can optionally be combined with {@link #excludes() excludes} or
* a custom {@link #predicate() predicate}. * a custom {@link #predicate() predicate}.
* <p>The default is empty, leading to a retry attempt for any exception. * <p>The default is empty, leading to a retry attempt for any exception.
@ -76,6 +80,9 @@ public @interface Retryable {
/** /**
* Non-applicable exception types to avoid a retry for. This attribute * Non-applicable exception types to avoid a retry for. This attribute
* allows for the convenient specification of assignable exception types. * allows for the convenient specification of assignable exception types.
* <p>The supplied exception types will be matched against an exception
* thrown by a failed invocation as well as nested
* {@linkplain Throwable#getCause() causes}.
* <p>This can optionally be combined with {@link #includes() includes} or * <p>This can optionally be combined with {@link #includes() includes} or
* a custom {@link #predicate() predicate}. * a custom {@link #predicate() predicate}.
* <p>The default is empty, leading to a retry attempt for any exception. * <p>The default is empty, leading to a retry attempt for any exception.

2
spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java

@ -65,7 +65,7 @@ public record MethodRetrySpec(
MethodRetryPredicate combinedPredicate() { MethodRetryPredicate combinedPredicate() {
ExceptionTypeFilter exceptionFilter = new ExceptionTypeFilter(this.includes, this.excludes); ExceptionTypeFilter exceptionFilter = new ExceptionTypeFilter(this.includes, this.excludes);
return (method, throwable) -> exceptionFilter.match(throwable) && return (method, throwable) -> exceptionFilter.match(throwable, true) &&
this.predicate.shouldRetry(method, throwable); this.predicate.shouldRetry(method, throwable);
} }

50
spring-context/src/test/java/org/springframework/resilience/ReactiveRetryInterceptorTests.java

@ -17,7 +17,7 @@
package org.springframework.resilience; package org.springframework.resilience;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method; import java.nio.charset.MalformedInputException;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.nio.file.FileSystemException; import java.nio.file.FileSystemException;
import java.time.Duration; import java.time.Duration;
@ -35,7 +35,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor; import org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor;
import org.springframework.resilience.annotation.Retryable; import org.springframework.resilience.annotation.Retryable;
import org.springframework.resilience.retry.MethodRetryPredicate;
import org.springframework.resilience.retry.MethodRetrySpec; import org.springframework.resilience.retry.MethodRetrySpec;
import org.springframework.resilience.retry.SimpleRetryInterceptor; import org.springframework.resilience.retry.SimpleRetryInterceptor;
@ -96,13 +95,16 @@ class ReactiveRetryInterceptorTests {
// Exact includes match: IOException // Exact includes match: IOException
assertThatRuntimeException() assertThatRuntimeException()
.isThrownBy(() -> proxy.ioOperation().block()) .isThrownBy(() -> proxy.ioOperation().block())
// Does NOT throw a RetryExhaustedException, because IOException3Predicate // Does NOT throw a RetryExhaustedException, because RejectMalformedInputException3Predicate
// returns false once the exception's message is "3". // rejects a retry if the last exception was a MalformedInputException with message "3".
.satisfies(isReactiveException()) .satisfies(isReactiveException())
.havingCause() .havingCause()
.isInstanceOf(IOException.class) .isInstanceOf(MalformedInputException.class)
.withMessage("3"); .withMessageContaining("3");
// 1 initial attempt + 2 retries
// 3 = 1 initial invocation + 2 retry attempts
// Not 3 retry attempts, because RejectMalformedInputException3Predicate rejects
// a retry if the last exception was a MalformedInputException with message "3".
assertThat(target.counter.get()).isEqualTo(3); assertThat(target.counter.get()).isEqualTo(3);
} }
@ -120,6 +122,22 @@ class ReactiveRetryInterceptorTests {
assertThat(target.counter.get()).isEqualTo(4); assertThat(target.counter.get()).isEqualTo(4);
} }
@Test // gh-35583
void withPostProcessorForClassWithCauseIncludesMatch() {
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
// Subtype includes match: FileSystemException
assertThatRuntimeException()
.isThrownBy(() -> proxy.fileSystemOperationWithNestedException().block())
.satisfies(isRetryExhaustedException())
.havingCause()
.isExactlyInstanceOf(RuntimeException.class)
.withCauseExactlyInstanceOf(FileSystemException.class);
// 1 initial attempt + 3 retries
assertThat(target.counter.get()).isEqualTo(4);
}
@Test @Test
void withPostProcessorForClassWithExcludesMatch() { void withPostProcessorForClassWithExcludesMatch() {
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean(); AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
@ -350,7 +368,7 @@ class ReactiveRetryInterceptorTests {
@Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40, @Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40,
includes = IOException.class, excludes = AccessDeniedException.class, includes = IOException.class, excludes = AccessDeniedException.class,
predicate = IOException3Predicate.class) predicate = RejectMalformedInputException3Predicate.class)
static class AnnotatedClassBean { static class AnnotatedClassBean {
AtomicInteger counter = new AtomicInteger(); AtomicInteger counter = new AtomicInteger();
@ -358,6 +376,9 @@ class ReactiveRetryInterceptorTests {
public Mono<Object> ioOperation() { public Mono<Object> ioOperation() {
return Mono.fromCallable(() -> { return Mono.fromCallable(() -> {
counter.incrementAndGet(); counter.incrementAndGet();
if (counter.get() == 3) {
throw new MalformedInputException(counter.get());
}
throw new IOException(counter.toString()); throw new IOException(counter.toString());
}); });
} }
@ -369,6 +390,13 @@ class ReactiveRetryInterceptorTests {
}); });
} }
public Mono<Object> fileSystemOperationWithNestedException() {
return Mono.fromCallable(() -> {
counter.incrementAndGet();
throw new RuntimeException(new FileSystemException(counter.toString()));
});
}
public Mono<Object> accessOperation() { public Mono<Object> accessOperation() {
return Mono.fromCallable(() -> { return Mono.fromCallable(() -> {
counter.incrementAndGet(); counter.incrementAndGet();
@ -393,13 +421,7 @@ class ReactiveRetryInterceptorTests {
} }
private static class IOException3Predicate implements MethodRetryPredicate {
@Override
public boolean shouldRetry(Method method, Throwable throwable) {
return !(throwable.getClass() == IOException.class && "3".equals(throwable.getMessage()));
}
}
// Bean classes for boundary testing // Bean classes for boundary testing

31
spring-context/src/test/java/org/springframework/resilience/RejectMalformedInputException3Predicate.java

@ -0,0 +1,31 @@
/*
* 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.resilience;
import java.lang.reflect.Method;
import java.nio.charset.MalformedInputException;
import org.springframework.resilience.retry.MethodRetryPredicate;
class RejectMalformedInputException3Predicate implements MethodRetryPredicate {
@Override
public boolean shouldRetry(Method method, Throwable throwable) {
return !(throwable.getClass() == MalformedInputException.class && throwable.getMessage().contains("3"));
}
}

56
spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java

@ -18,7 +18,7 @@ package org.springframework.resilience;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.nio.charset.MalformedInputException;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.time.Duration; import java.time.Duration;
import java.util.Properties; import java.util.Properties;
@ -42,15 +42,16 @@ import org.springframework.resilience.annotation.ConcurrencyLimit;
import org.springframework.resilience.annotation.EnableResilientMethods; import org.springframework.resilience.annotation.EnableResilientMethods;
import org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor; import org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor;
import org.springframework.resilience.annotation.Retryable; import org.springframework.resilience.annotation.Retryable;
import org.springframework.resilience.retry.MethodRetryPredicate;
import org.springframework.resilience.retry.MethodRetrySpec; import org.springframework.resilience.retry.MethodRetrySpec;
import org.springframework.resilience.retry.SimpleRetryInterceptor; import org.springframework.resilience.retry.SimpleRetryInterceptor;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException; import static org.assertj.core.api.Assertions.assertThatIOException;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
/** /**
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 7.0 * @since 7.0
*/ */
class RetryInterceptorTests { class RetryInterceptorTests {
@ -187,12 +188,22 @@ class RetryInterceptorTests {
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class); AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class);
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy); AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3"); // 3 = 1 initial invocation + 2 retry attempts
// Not 3 retry attempts, because RejectMalformedInputException3Predicate rejects
// a retry if the last exception was a MalformedInputException with message "3".
assertThatIOException().isThrownBy(proxy::retryOperation).withMessageContaining("3");
assertThat(target.counter).isEqualTo(3); assertThat(target.counter).isEqualTo(3);
// 7 = 3 + 1 initial invocation + 3 retry attempts
assertThatRuntimeException()
.isThrownBy(proxy::retryOperationWithNestedException)
.havingCause()
.isExactlyInstanceOf(IOException.class)
.withMessage("7");
assertThat(target.counter).isEqualTo(7);
assertThatIOException().isThrownBy(proxy::otherOperation); assertThatIOException().isThrownBy(proxy::otherOperation);
assertThat(target.counter).isEqualTo(4); assertThat(target.counter).isEqualTo(8);
assertThatIOException().isThrownBy(proxy::overrideOperation); assertThatIOException().isThrownBy(proxy::overrideOperation);
assertThat(target.counter).isEqualTo(6); assertThat(target.counter).isEqualTo(10);
} }
@Test @Test
@ -212,7 +223,10 @@ class RetryInterceptorTests {
AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class); AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class);
AnnotatedClassBeanWithStrings target = (AnnotatedClassBeanWithStrings) AopProxyUtils.getSingletonTarget(proxy); AnnotatedClassBeanWithStrings target = (AnnotatedClassBeanWithStrings) AopProxyUtils.getSingletonTarget(proxy);
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3"); // 3 = 1 initial invocation + 2 retry attempts
// Not 3 retry attempts, because RejectMalformedInputException3Predicate rejects
// a retry if the last exception was a MalformedInputException with message "3".
assertThatIOException().isThrownBy(proxy::retryOperation).withMessageContaining("3");
assertThat(target.counter).isEqualTo(3); assertThat(target.counter).isEqualTo(3);
assertThatIOException().isThrownBy(proxy::otherOperation); assertThatIOException().isThrownBy(proxy::otherOperation);
assertThat(target.counter).isEqualTo(4); assertThat(target.counter).isEqualTo(4);
@ -237,7 +251,10 @@ class RetryInterceptorTests {
AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class); AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class);
AnnotatedClassBeanWithStrings target = (AnnotatedClassBeanWithStrings) AopProxyUtils.getSingletonTarget(proxy); AnnotatedClassBeanWithStrings target = (AnnotatedClassBeanWithStrings) AopProxyUtils.getSingletonTarget(proxy);
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3"); // 3 = 1 initial invocation + 2 retry attempts
// Not 3 retry attempts, because RejectMalformedInputException3Predicate rejects
// a retry if the last exception was a MalformedInputException with message "3".
assertThatIOException().isThrownBy(proxy::retryOperation).withMessageContaining("3");
assertThat(target.counter).isEqualTo(3); assertThat(target.counter).isEqualTo(3);
assertThatIOException().isThrownBy(proxy::otherOperation); assertThatIOException().isThrownBy(proxy::otherOperation);
assertThat(target.counter).isEqualTo(4); assertThat(target.counter).isEqualTo(4);
@ -267,6 +284,7 @@ class RetryInterceptorTests {
int counter = 0; int counter = 0;
@Override
public void retryOperation() throws IOException { public void retryOperation() throws IOException {
counter++; counter++;
throw new IOException(Integer.toString(counter)); throw new IOException(Integer.toString(counter));
@ -314,16 +332,24 @@ class RetryInterceptorTests {
@Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40, @Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40,
includes = IOException.class, excludes = AccessDeniedException.class, includes = IOException.class, excludes = AccessDeniedException.class,
predicate = CustomPredicate.class) predicate = RejectMalformedInputException3Predicate.class)
static class AnnotatedClassBean { static class AnnotatedClassBean {
int counter = 0; int counter = 0;
public void retryOperation() throws IOException { public void retryOperation() throws IOException {
counter++; counter++;
if (counter == 3) {
throw new MalformedInputException(counter);
}
throw new IOException(Integer.toString(counter)); throw new IOException(Integer.toString(counter));
} }
public void retryOperationWithNestedException() {
counter++;
throw new RuntimeException(new IOException(Integer.toString(counter)));
}
public void otherOperation() throws IOException { public void otherOperation() throws IOException {
counter++; counter++;
throw new AccessDeniedException(Integer.toString(counter)); throw new AccessDeniedException(Integer.toString(counter));
@ -340,13 +366,16 @@ class RetryInterceptorTests {
@Retryable(delayString = "${delay}", jitterString = "${jitter}", @Retryable(delayString = "${delay}", jitterString = "${jitter}",
multiplierString = "${multiplier}", maxDelayString = "${maxDelay}", multiplierString = "${multiplier}", maxDelayString = "${maxDelay}",
includes = IOException.class, excludes = AccessDeniedException.class, includes = IOException.class, excludes = AccessDeniedException.class,
predicate = CustomPredicate.class) predicate = RejectMalformedInputException3Predicate.class)
static class AnnotatedClassBeanWithStrings { static class AnnotatedClassBeanWithStrings {
int counter = 0; int counter = 0;
public void retryOperation() throws IOException { public void retryOperation() throws IOException {
counter++; counter++;
if (counter == 3) {
throw new MalformedInputException(counter);
}
throw new IOException(Integer.toString(counter)); throw new IOException(Integer.toString(counter));
} }
@ -363,15 +392,6 @@ class RetryInterceptorTests {
} }
private static class CustomPredicate implements MethodRetryPredicate {
@Override
public boolean shouldRetry(Method method, Throwable throwable) {
return !"3".equals(throwable.getMessage());
}
}
static class DoubleAnnotatedBean { static class DoubleAnnotatedBean {
AtomicInteger current = new AtomicInteger(); AtomicInteger current = new AtomicInteger();

2
spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java

@ -58,7 +58,7 @@ class DefaultRetryPolicy implements RetryPolicy {
@Override @Override
public boolean shouldRetry(Throwable throwable) { public boolean shouldRetry(Throwable throwable) {
return (this.exceptionFilter.match(throwable) && return (this.exceptionFilter.match(throwable, true) &&
(this.predicate == null || this.predicate.test(throwable))); (this.predicate == null || this.predicate.test(throwable)));
} }

12
spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java

@ -296,6 +296,9 @@ public interface RetryPolicy {
* Specify the types of exceptions for which the {@link RetryPolicy} * Specify the types of exceptions for which the {@link RetryPolicy}
* should retry a failed operation. * should retry a failed operation.
* <p>Defaults to all exception types. * <p>Defaults to all exception types.
* <p>The supplied exception types will be matched against an exception
* thrown by a failed operation as well as nested
* {@linkplain Throwable#getCause() causes}.
* <p>If included exception types have already been configured, the supplied * <p>If included exception types have already been configured, the supplied
* types will be added to the existing list of included types. * types will be added to the existing list of included types.
* <p>This can be combined with other {@code includes}, {@code excludes}, * <p>This can be combined with other {@code includes}, {@code excludes},
@ -318,6 +321,9 @@ public interface RetryPolicy {
* Specify the types of exceptions for which the {@link RetryPolicy} * Specify the types of exceptions for which the {@link RetryPolicy}
* should retry a failed operation. * should retry a failed operation.
* <p>Defaults to all exception types. * <p>Defaults to all exception types.
* <p>The supplied exception types will be matched against an exception
* thrown by a failed operation as well as nested
* {@linkplain Throwable#getCause() causes}.
* <p>If included exception types have already been configured, the supplied * <p>If included exception types have already been configured, the supplied
* types will be added to the existing list of included types. * types will be added to the existing list of included types.
* <p>This can be combined with other {@code includes}, {@code excludes}, * <p>This can be combined with other {@code includes}, {@code excludes},
@ -337,6 +343,9 @@ public interface RetryPolicy {
/** /**
* Specify the types of exceptions for which the {@link RetryPolicy} * Specify the types of exceptions for which the {@link RetryPolicy}
* should not retry a failed operation. * should not retry a failed operation.
* <p>The supplied exception types will be matched against an exception
* thrown by a failed operation as well as nested
* {@linkplain Throwable#getCause() causes}.
* <p>If excluded exception types have already been configured, the supplied * <p>If excluded exception types have already been configured, the supplied
* types will be added to the existing list of excluded types. * types will be added to the existing list of excluded types.
* <p>This can be combined with {@code includes}, other {@code excludes}, * <p>This can be combined with {@code includes}, other {@code excludes},
@ -358,6 +367,9 @@ public interface RetryPolicy {
/** /**
* Specify the types of exceptions for which the {@link RetryPolicy} * Specify the types of exceptions for which the {@link RetryPolicy}
* should not retry a failed operation. * should not retry a failed operation.
* <p>The supplied exception types will be matched against an exception
* thrown by a failed operation as well as nested
* {@linkplain Throwable#getCause() causes}.
* <p>If excluded exception types have already been configured, the supplied * <p>If excluded exception types have already been configured, the supplied
* types will be added to the existing list of excluded types. * types will be added to the existing list of excluded types.
* <p>This can be combined with {@code includes}, other {@code excludes}, * <p>This can be combined with {@code includes}, other {@code excludes},

13
spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java

@ -369,7 +369,7 @@ class RetryTemplateTests {
public String execute() throws Exception { public String execute() throws Exception {
return switch (invocationCount.incrementAndGet()) { return switch (invocationCount.incrementAndGet()) {
case 1 -> throw new IOException(); case 1 -> throw new IOException();
case 2 -> throw new IOException(); case 2 -> throw new RuntimeException(new IOException());
case 3 -> throw new CustomFileNotFoundException(); case 3 -> throw new CustomFileNotFoundException();
default -> "success"; default -> "success";
}; };
@ -388,14 +388,15 @@ class RetryTemplateTests {
.withCauseExactlyInstanceOf(CustomFileNotFoundException.class) .withCauseExactlyInstanceOf(CustomFileNotFoundException.class)
.satisfies(hasSuppressedExceptionsSatisfyingExactly( .satisfies(hasSuppressedExceptionsSatisfyingExactly(
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class), suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class) suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(RuntimeException.class)
.hasCauseExactlyInstanceOf(IOException.class)
)) ))
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2)) .satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
.satisfies(throwable -> { .satisfies(throwable -> {
repeat(2, () -> { inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(RuntimeException.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class)); inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
}); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(CustomFileNotFoundException.class));
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable); inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
}); });
// 3 = 1 initial invocation + 2 retry attempts // 3 = 1 initial invocation + 2 retry attempts

Loading…
Cancel
Save