mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
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
This commit is contained in:
+36
-14
@@ -17,7 +17,7 @@
|
||||
package org.springframework.resilience;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.FileSystemException;
|
||||
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.resilience.annotation.RetryAnnotationBeanPostProcessor;
|
||||
import org.springframework.resilience.annotation.Retryable;
|
||||
import org.springframework.resilience.retry.MethodRetryPredicate;
|
||||
import org.springframework.resilience.retry.MethodRetrySpec;
|
||||
import org.springframework.resilience.retry.SimpleRetryInterceptor;
|
||||
|
||||
@@ -96,13 +95,16 @@ class ReactiveRetryInterceptorTests {
|
||||
// Exact includes match: IOException
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.ioOperation().block())
|
||||
// Does NOT throw a RetryExhaustedException, because IOException3Predicate
|
||||
// returns false once the exception's message is "3".
|
||||
// Does NOT throw a RetryExhaustedException, because RejectMalformedInputException3Predicate
|
||||
// rejects a retry if the last exception was a MalformedInputException with message "3".
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("3");
|
||||
// 1 initial attempt + 2 retries
|
||||
.isInstanceOf(MalformedInputException.class)
|
||||
.withMessageContaining("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".
|
||||
assertThat(target.counter.get()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@@ -120,6 +122,22 @@ class ReactiveRetryInterceptorTests {
|
||||
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
|
||||
void withPostProcessorForClassWithExcludesMatch() {
|
||||
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
|
||||
@@ -350,7 +368,7 @@ class ReactiveRetryInterceptorTests {
|
||||
|
||||
@Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40,
|
||||
includes = IOException.class, excludes = AccessDeniedException.class,
|
||||
predicate = IOException3Predicate.class)
|
||||
predicate = RejectMalformedInputException3Predicate.class)
|
||||
static class AnnotatedClassBean {
|
||||
|
||||
AtomicInteger counter = new AtomicInteger();
|
||||
@@ -358,6 +376,9 @@ class ReactiveRetryInterceptorTests {
|
||||
public Mono<Object> ioOperation() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
if (counter.get() == 3) {
|
||||
throw new MalformedInputException(counter.get());
|
||||
}
|
||||
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() {
|
||||
return Mono.fromCallable(() -> {
|
||||
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
|
||||
|
||||
+31
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
+38
-18
@@ -18,7 +18,7 @@ package org.springframework.resilience;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.time.Duration;
|
||||
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.RetryAnnotationBeanPostProcessor;
|
||||
import org.springframework.resilience.annotation.Retryable;
|
||||
import org.springframework.resilience.retry.MethodRetryPredicate;
|
||||
import org.springframework.resilience.retry.MethodRetrySpec;
|
||||
import org.springframework.resilience.retry.SimpleRetryInterceptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
|
||||
|
||||
/**
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
*/
|
||||
class RetryInterceptorTests {
|
||||
@@ -187,12 +188,22 @@ class RetryInterceptorTests {
|
||||
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class);
|
||||
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);
|
||||
// 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);
|
||||
assertThat(target.counter).isEqualTo(4);
|
||||
assertThat(target.counter).isEqualTo(8);
|
||||
assertThatIOException().isThrownBy(proxy::overrideOperation);
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
assertThat(target.counter).isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,7 +223,10 @@ class RetryInterceptorTests {
|
||||
AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class);
|
||||
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);
|
||||
assertThatIOException().isThrownBy(proxy::otherOperation);
|
||||
assertThat(target.counter).isEqualTo(4);
|
||||
@@ -237,7 +251,10 @@ class RetryInterceptorTests {
|
||||
AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class);
|
||||
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);
|
||||
assertThatIOException().isThrownBy(proxy::otherOperation);
|
||||
assertThat(target.counter).isEqualTo(4);
|
||||
@@ -267,6 +284,7 @@ class RetryInterceptorTests {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
@Override
|
||||
public void retryOperation() throws IOException {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
@@ -314,16 +332,24 @@ class RetryInterceptorTests {
|
||||
|
||||
@Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40,
|
||||
includes = IOException.class, excludes = AccessDeniedException.class,
|
||||
predicate = CustomPredicate.class)
|
||||
predicate = RejectMalformedInputException3Predicate.class)
|
||||
static class AnnotatedClassBean {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
public void retryOperation() throws IOException {
|
||||
counter++;
|
||||
if (counter == 3) {
|
||||
throw new MalformedInputException(counter);
|
||||
}
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
public void retryOperationWithNestedException() {
|
||||
counter++;
|
||||
throw new RuntimeException(new IOException(Integer.toString(counter)));
|
||||
}
|
||||
|
||||
public void otherOperation() throws IOException {
|
||||
counter++;
|
||||
throw new AccessDeniedException(Integer.toString(counter));
|
||||
@@ -340,13 +366,16 @@ class RetryInterceptorTests {
|
||||
@Retryable(delayString = "${delay}", jitterString = "${jitter}",
|
||||
multiplierString = "${multiplier}", maxDelayString = "${maxDelay}",
|
||||
includes = IOException.class, excludes = AccessDeniedException.class,
|
||||
predicate = CustomPredicate.class)
|
||||
predicate = RejectMalformedInputException3Predicate.class)
|
||||
static class AnnotatedClassBeanWithStrings {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
public void retryOperation() throws IOException {
|
||||
counter++;
|
||||
if (counter == 3) {
|
||||
throw new MalformedInputException(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 {
|
||||
|
||||
AtomicInteger current = new AtomicInteger();
|
||||
|
||||
Reference in New Issue
Block a user