Introduce @ConcurrencyLimit annotation based on ConcurrencyThrottleInterceptor
Moves @Retryable infrastructure to resilience package in spring-context module. Includes duration parsing and placeholder resolution for @Retryable attributes. Provides convenient @EnableResilientMethods for @Retryable + @ConcurrencyLimit. Closes gh-35133 See gh-34529
This commit is contained in:
parent
3ce7613195
commit
c9078bfe14
|
@ -41,7 +41,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||||
public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends AbstractAdvisingBeanPostProcessor
|
public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends AbstractAdvisingBeanPostProcessor
|
||||||
implements BeanFactoryAware {
|
implements BeanFactoryAware {
|
||||||
|
|
||||||
private @Nullable ConfigurableListableBeanFactory beanFactory;
|
protected @Nullable ConfigurableListableBeanFactory beanFactory;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -44,10 +44,24 @@ import org.springframework.util.ConcurrencyThrottleSupport;
|
||||||
public class ConcurrencyThrottleInterceptor extends ConcurrencyThrottleSupport
|
public class ConcurrencyThrottleInterceptor extends ConcurrencyThrottleSupport
|
||||||
implements MethodInterceptor, Serializable {
|
implements MethodInterceptor, Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a default {@code ConcurrencyThrottleInterceptor}
|
||||||
|
* with concurrency limit 1.
|
||||||
|
*/
|
||||||
public ConcurrencyThrottleInterceptor() {
|
public ConcurrencyThrottleInterceptor() {
|
||||||
setConcurrencyLimit(1);
|
this(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a default {@code ConcurrencyThrottleInterceptor}
|
||||||
|
* with the given concurrency limit.
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public ConcurrencyThrottleInterceptor(int concurrencyLimit) {
|
||||||
|
setConcurrencyLimit(concurrencyLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object invoke(MethodInvocation methodInvocation) throws Throwable {
|
public @Nullable Object invoke(MethodInvocation methodInvocation) throws Throwable {
|
||||||
beforeAccess();
|
beforeAccess();
|
||||||
|
|
|
@ -1,46 +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.aop.retry.annotation;
|
|
||||||
|
|
||||||
import org.springframework.aop.Pointcut;
|
|
||||||
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor;
|
|
||||||
import org.springframework.aop.support.ComposablePointcut;
|
|
||||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
|
||||||
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor
|
|
||||||
* BeanPostProcessor} that applies {@link RetryAnnotationInterceptor}
|
|
||||||
* to all bean methods annotated with {@link Retryable} annotations.
|
|
||||||
*
|
|
||||||
* @author Juergen Hoeller
|
|
||||||
* @since 7.0
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
|
|
||||||
|
|
||||||
public RetryAnnotationBeanPostProcessor() {
|
|
||||||
setBeforeExistingAdvisors(true);
|
|
||||||
|
|
||||||
Pointcut cpc = new AnnotationMatchingPointcut(Retryable.class, true);
|
|
||||||
Pointcut mpc = new AnnotationMatchingPointcut(null, Retryable.class, true);
|
|
||||||
this.advisor = new DefaultPointcutAdvisor(
|
|
||||||
new ComposablePointcut(cpc).union(mpc),
|
|
||||||
new RetryAnnotationInterceptor());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,96 +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.aop.retry.annotation;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
|
|
||||||
import org.springframework.aop.retry.AbstractRetryInterceptor;
|
|
||||||
import org.springframework.aop.retry.MethodRetryPredicate;
|
|
||||||
import org.springframework.aop.retry.MethodRetrySpec;
|
|
||||||
import org.springframework.core.MethodClassKey;
|
|
||||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
|
||||||
import org.springframework.util.ReflectionUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An annotation-based retry interceptor based on {@link Retryable} annotations.
|
|
||||||
*
|
|
||||||
* @author Juergen Hoeller
|
|
||||||
* @since 7.0
|
|
||||||
*/
|
|
||||||
public class RetryAnnotationInterceptor extends AbstractRetryInterceptor {
|
|
||||||
|
|
||||||
private final Map<MethodClassKey, MethodRetrySpec> retrySpecCache = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected @Nullable MethodRetrySpec getRetrySpec(Method method, Class<?> targetClass) {
|
|
||||||
MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
|
|
||||||
MethodRetrySpec retrySpec = this.retrySpecCache.get(cacheKey);
|
|
||||||
if (retrySpec != null) {
|
|
||||||
return retrySpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
Retryable retryable = AnnotatedElementUtils.getMergedAnnotation(method, Retryable.class);
|
|
||||||
if (retryable == null) {
|
|
||||||
retryable = AnnotatedElementUtils.getMergedAnnotation(targetClass, Retryable.class);
|
|
||||||
if (retryable == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeUnit timeUnit = retryable.timeUnit();
|
|
||||||
retrySpec = new MethodRetrySpec(
|
|
||||||
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()),
|
|
||||||
instantiatePredicate(retryable.predicate()), retryable.maxAttempts(),
|
|
||||||
toDuration(retryable.delay(), timeUnit), toDuration(retryable.jitter(), timeUnit),
|
|
||||||
retryable.multiplier(), toDuration(retryable.maxDelay(), timeUnit));
|
|
||||||
|
|
||||||
MethodRetrySpec existing = this.retrySpecCache.putIfAbsent(cacheKey, retrySpec);
|
|
||||||
return (existing != null ? existing : retrySpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) {
|
|
||||||
if (predicateClass == MethodRetryPredicate.class) {
|
|
||||||
return (method, throwable) -> true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return ReflectionUtils.accessibleConstructor(predicateClass).newInstance();
|
|
||||||
}
|
|
||||||
catch (Throwable ex) {
|
|
||||||
throw new IllegalStateException("Failed to instantiate predicate class [" + predicateClass + "]", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Duration toDuration(long value, TimeUnit timeUnit) {
|
|
||||||
try {
|
|
||||||
return Duration.of(value, timeUnit.toChronoUnit());
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unsupported unit " + timeUnit + " for value \"" + value + "\": " + ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,155 +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.aop.retry.annotation;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import org.springframework.aop.retry.MethodRetryPredicate;
|
|
||||||
import org.springframework.aot.hint.annotation.Reflective;
|
|
||||||
import org.springframework.core.annotation.AliasFor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A common annotation specifying retry characteristics for an individual method,
|
|
||||||
* or for all proxy-invoked methods in a given class hierarchy if annotated at
|
|
||||||
* the type level.
|
|
||||||
*
|
|
||||||
* <p>Aligned with {@link org.springframework.core.retry.RetryTemplate}
|
|
||||||
* as well as Reactor's retry support, either re-invoking an imperative
|
|
||||||
* target method or decorating a reactive result accordingly.
|
|
||||||
*
|
|
||||||
* <p>Inspired by the <a href="https://github.com/spring-projects/spring-retry">Spring Retry</a>
|
|
||||||
* project but redesigned as a minimal core retry feature in the Spring Framework.
|
|
||||||
*
|
|
||||||
* @author Juergen Hoeller
|
|
||||||
* @since 7.0
|
|
||||||
* @see RetryAnnotationBeanPostProcessor
|
|
||||||
* @see RetryAnnotationInterceptor
|
|
||||||
* @see org.springframework.core.retry.RetryPolicy
|
|
||||||
* @see org.springframework.core.retry.RetryTemplate
|
|
||||||
* @see reactor.core.publisher.Mono#retryWhen
|
|
||||||
* @see reactor.core.publisher.Flux#retryWhen
|
|
||||||
*/
|
|
||||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
@Reflective
|
|
||||||
public @interface Retryable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenient default attribute for {@link #includes()},
|
|
||||||
* typically used with a single exception type to retry for.
|
|
||||||
*/
|
|
||||||
@AliasFor("includes")
|
|
||||||
Class<? extends Throwable>[] value() default {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applicable exception types to attempt a retry for. This attribute
|
|
||||||
* allows for the convenient specification of assignable exception types.
|
|
||||||
* <p>The default is empty, leading to a retry attempt for any exception.
|
|
||||||
* @see #excludes()
|
|
||||||
* @see #predicate()
|
|
||||||
*/
|
|
||||||
@AliasFor("value")
|
|
||||||
Class<? extends Throwable>[] includes() default {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Non-applicable exception types to avoid a retry for. This attribute
|
|
||||||
* allows for the convenient specification of assignable exception types.
|
|
||||||
* <p>The default is empty, leading to a retry attempt for any exception.
|
|
||||||
* @see #includes()
|
|
||||||
* @see #predicate()
|
|
||||||
*/
|
|
||||||
Class<? extends Throwable>[] excludes() default {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A predicate for filtering applicable exceptions for which
|
|
||||||
* an invocation can be retried.
|
|
||||||
* <p>The default is a retry attempt for any exception.
|
|
||||||
* @see #includes()
|
|
||||||
* @see #excludes()
|
|
||||||
*/
|
|
||||||
Class<? extends MethodRetryPredicate> predicate() default MethodRetryPredicate.class;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum number of retry attempts, in addition to the initial invocation.
|
|
||||||
* <p>The default is 3.
|
|
||||||
*/
|
|
||||||
long maxAttempts() default 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base delay after the initial invocation. If a multiplier is specified,
|
|
||||||
* this serves as the initial delay to multiply from.
|
|
||||||
* <p>The time unit is milliseconds by default but can be overridden via
|
|
||||||
* {@link #timeUnit}.
|
|
||||||
* <p>The default is 1000.
|
|
||||||
* @see #jitter()
|
|
||||||
* @see #multiplier()
|
|
||||||
* @see #maxDelay()
|
|
||||||
*/
|
|
||||||
long delay() default 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 base {@link #delay()} or
|
|
||||||
* above {@link #maxDelay()}. If a multiplier is specified, it is applied
|
|
||||||
* to the jitter value as well.
|
|
||||||
* <p>The time unit is milliseconds by default but can be overridden via
|
|
||||||
* {@link #timeUnit}.
|
|
||||||
* <p>The default is 0 (no jitter).
|
|
||||||
* @see #delay()
|
|
||||||
* @see #multiplier()
|
|
||||||
* @see #maxDelay()
|
|
||||||
*/
|
|
||||||
long jitter() default 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A multiplier for a delay for the next retry attempt, applied
|
|
||||||
* to the previous delay (starting with {@link #delay()}) as well
|
|
||||||
* as to the applicable {@link #jitter()} for each attempt.
|
|
||||||
* <p>The default is 1.0, effectively resulting in a fixed delay.
|
|
||||||
* @see #delay()
|
|
||||||
* @see #jitter()
|
|
||||||
* @see #maxDelay()
|
|
||||||
*/
|
|
||||||
double multiplier() default 1.0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum delay for any retry attempt, limiting how far {@link #jitter()}
|
|
||||||
* and {@link #multiplier()} can increase the {@linkplain #delay() delay}.
|
|
||||||
* <p>The time unit is milliseconds by default but can be overridden via
|
|
||||||
* {@link #timeUnit}.
|
|
||||||
* <p>The default is unlimited.
|
|
||||||
* @see #delay()
|
|
||||||
* @see #jitter()
|
|
||||||
* @see #multiplier()
|
|
||||||
*/
|
|
||||||
long maxDelay() default Long.MAX_VALUE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link TimeUnit} to use for {@link #delay}, {@link #jitter},
|
|
||||||
* and {@link #maxDelay}.
|
|
||||||
* <p>The default is {@link TimeUnit#MILLISECONDS}.
|
|
||||||
*/
|
|
||||||
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
/**
|
|
||||||
* Annotation-based retry support for common Spring setups.
|
|
||||||
*/
|
|
||||||
@NullMarked
|
|
||||||
package org.springframework.aop.retry.annotation;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.aot.hint.annotation.Reflective;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common annotation specifying a concurrency limit for an individual method,
|
||||||
|
* or for all proxy-invoked methods in a given class hierarchy if annotated at
|
||||||
|
* the type level.
|
||||||
|
*
|
||||||
|
* <p>In the type-level case, all methods inheriting the concurrency limit
|
||||||
|
* from the type level share a common concurrency throttle, with any mix
|
||||||
|
* of such method invocations contributing to the shared concurrency limit.
|
||||||
|
* Whereas for a locally annotated method, a local throttle with the specified
|
||||||
|
* limit is going to be applied to invocations of that particular method only.
|
||||||
|
*
|
||||||
|
* <p>This is particularly useful with Virtual Threads where there is generally
|
||||||
|
* no thread pool limit in place. For asynchronous tasks, this can be constrained
|
||||||
|
* on {@link org.springframework.core.task.SimpleAsyncTaskExecutor}; for
|
||||||
|
* synchronous invocations, this annotation provides equivalent behavior through
|
||||||
|
* {@link org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor}.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 7.0
|
||||||
|
* @see EnableResilientMethods
|
||||||
|
* @see ConcurrencyLimitBeanPostProcessor
|
||||||
|
* @see org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor
|
||||||
|
* @see org.springframework.core.task.SimpleAsyncTaskExecutor#setConcurrencyLimit
|
||||||
|
*/
|
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Reflective
|
||||||
|
public @interface ConcurrencyLimit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The applicable concurrency limit: 1 by default,
|
||||||
|
* effectively locking the target instance for each method invocation.
|
||||||
|
* <p>Specify a limit higher than 1 for pool-like throttling, constraining
|
||||||
|
* the number of concurrent invocations similar to the upper bound of a pool.
|
||||||
|
*/
|
||||||
|
int value() default 1;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* 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.annotation;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.aopalliance.intercept.Joinpoint;
|
||||||
|
import org.aopalliance.intercept.MethodInterceptor;
|
||||||
|
import org.aopalliance.intercept.MethodInvocation;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.aop.Pointcut;
|
||||||
|
import org.springframework.aop.ProxyMethodInvocation;
|
||||||
|
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor;
|
||||||
|
import org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor;
|
||||||
|
import org.springframework.aop.support.ComposablePointcut;
|
||||||
|
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||||
|
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
||||||
|
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor
|
||||||
|
* BeanPostProcessor} that applies a concurrency interceptor to all bean methods
|
||||||
|
* annotated with {@link ConcurrencyLimit} annotations.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
|
||||||
|
|
||||||
|
public ConcurrencyLimitBeanPostProcessor() {
|
||||||
|
setBeforeExistingAdvisors(true);
|
||||||
|
|
||||||
|
Pointcut cpc = new AnnotationMatchingPointcut(ConcurrencyLimit.class, true);
|
||||||
|
Pointcut mpc = new AnnotationMatchingPointcut(null, ConcurrencyLimit.class, true);
|
||||||
|
this.advisor = new DefaultPointcutAdvisor(
|
||||||
|
new ComposablePointcut(cpc).union(mpc),
|
||||||
|
new ConcurrencyLimitInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class ConcurrencyLimitInterceptor implements MethodInterceptor {
|
||||||
|
|
||||||
|
private final Map<Object, ConcurrencyThrottleCache> cachePerInstance =
|
||||||
|
new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
|
||||||
|
Method method = invocation.getMethod();
|
||||||
|
Object target = invocation.getThis();
|
||||||
|
Class<?> targetClass = (target != null ? target.getClass() : method.getDeclaringClass());
|
||||||
|
if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) {
|
||||||
|
// Allow validation for AOP proxy without a target
|
||||||
|
target = methodInvocation.getProxy();
|
||||||
|
}
|
||||||
|
Assert.state(target != null, "Target must not be null");
|
||||||
|
|
||||||
|
ConcurrencyThrottleCache cache = this.cachePerInstance.computeIfAbsent(target,
|
||||||
|
k -> new ConcurrencyThrottleCache());
|
||||||
|
MethodInterceptor interceptor = cache.methodInterceptors.get(method);
|
||||||
|
if (interceptor == null) {
|
||||||
|
synchronized (cache) {
|
||||||
|
interceptor = cache.methodInterceptors.get(method);
|
||||||
|
if (interceptor == null) {
|
||||||
|
boolean perMethod = false;
|
||||||
|
ConcurrencyLimit limit = AnnotatedElementUtils.getMergedAnnotation(method, ConcurrencyLimit.class);
|
||||||
|
if (limit != null) {
|
||||||
|
perMethod = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
interceptor = cache.classInterceptor;
|
||||||
|
if (interceptor == null) {
|
||||||
|
limit = AnnotatedElementUtils.getMergedAnnotation(targetClass, ConcurrencyLimit.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interceptor == null) {
|
||||||
|
interceptor = (limit != null ? new ConcurrencyThrottleInterceptor(limit.value()) :
|
||||||
|
Joinpoint::proceed);
|
||||||
|
if (!perMethod) {
|
||||||
|
cache.classInterceptor = interceptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.methodInterceptors.put(method, interceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interceptor.invoke(invocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class ConcurrencyThrottleCache {
|
||||||
|
|
||||||
|
final Map<Method, MethodInterceptor> methodInterceptors = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Nullable MethodInterceptor classInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* 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.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables Spring's core resilience features for method invocations:
|
||||||
|
* {@link Retryable} as well as {@link ConcurrencyLimit}.
|
||||||
|
*
|
||||||
|
* <p>These annotations can also be individually enabled through
|
||||||
|
* defining a {@link RetryAnnotationBeanPostProcessor} and/or a
|
||||||
|
* {@link ConcurrencyLimitBeanPostProcessor}.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 7.0
|
||||||
|
* @see RetryAnnotationBeanPostProcessor
|
||||||
|
* @see ConcurrencyLimitBeanPostProcessor
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Import(ResilientMethodsConfiguration.class)
|
||||||
|
public @interface EnableResilientMethods {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
|
||||||
|
* to standard Java interface-based proxies.
|
||||||
|
* <p>The default is {@code false}.
|
||||||
|
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
|
||||||
|
* Spring-managed beans requiring proxying, not just those marked with {@code @Retryable}
|
||||||
|
* or {@code @ConcurrencyLimit}. For example, other beans marked with Spring's
|
||||||
|
* {@code @Transactional} annotation will be upgraded to subclass proxying at
|
||||||
|
* the same time. This approach has no negative impact in practice unless one is
|
||||||
|
* explicitly expecting one type of proxy vs. another — for example, in tests.
|
||||||
|
*/
|
||||||
|
boolean proxyTargetClass() default false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate the order in which the {@link RetryAnnotationBeanPostProcessor}
|
||||||
|
* and {@link ConcurrencyLimitBeanPostProcessor} should be applied.
|
||||||
|
* <p>The default is {@link Ordered#LOWEST_PRECEDENCE} in order to run
|
||||||
|
* after all other post-processors, so that it can add an advisor to
|
||||||
|
* existing proxies rather than double-proxy.
|
||||||
|
*/
|
||||||
|
int order() default Ordered.LOWEST_PRECEDENCE;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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.annotation;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.aop.framework.ProxyProcessorSupport;
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.ImportAware;
|
||||||
|
import org.springframework.context.annotation.Role;
|
||||||
|
import org.springframework.core.annotation.AnnotationAttributes;
|
||||||
|
import org.springframework.core.type.AnnotationMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code @Configuration} class that registers the Spring infrastructure beans necessary
|
||||||
|
* to enable proxy-based method invocations with retry and concurrency limit behavior.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 7.0
|
||||||
|
* @see EnableResilientMethods
|
||||||
|
* @see RetryAnnotationBeanPostProcessor
|
||||||
|
* @see ConcurrencyLimitBeanPostProcessor
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
|
public class ResilientMethodsConfiguration implements ImportAware {
|
||||||
|
|
||||||
|
private @Nullable AnnotationAttributes enableResilientMethods;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||||
|
this.enableResilientMethods = AnnotationAttributes.fromMap(
|
||||||
|
importMetadata.getAnnotationAttributes(EnableResilientMethods.class.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureProxySupport(ProxyProcessorSupport proxySupport) {
|
||||||
|
if (this.enableResilientMethods != null) {
|
||||||
|
proxySupport.setProxyTargetClass(this.enableResilientMethods.getBoolean("proxyTargetClass"));
|
||||||
|
proxySupport.setOrder(this.enableResilientMethods.<Integer>getNumber("order"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean(name = "org.springframework.resilience.annotation.internalRetryAnnotationProcessor")
|
||||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
|
public RetryAnnotationBeanPostProcessor retryAdvisor() {
|
||||||
|
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||||
|
configureProxySupport(bpp);
|
||||||
|
return bpp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "org.springframework.resilience.annotation.internalConcurrencyLimitProcessor")
|
||||||
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
|
public ConcurrencyLimitBeanPostProcessor concurrencyLimitAdvisor() {
|
||||||
|
ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor();
|
||||||
|
configureProxySupport(bpp);
|
||||||
|
return bpp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
* 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.annotation;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.aop.Pointcut;
|
||||||
|
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor;
|
||||||
|
import org.springframework.aop.support.ComposablePointcut;
|
||||||
|
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||||
|
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
||||||
|
import org.springframework.context.EmbeddedValueResolverAware;
|
||||||
|
import org.springframework.core.MethodClassKey;
|
||||||
|
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||||
|
import org.springframework.format.annotation.DurationFormat;
|
||||||
|
import org.springframework.format.datetime.standard.DurationFormatterUtils;
|
||||||
|
import org.springframework.resilience.retry.AbstractRetryInterceptor;
|
||||||
|
import org.springframework.resilience.retry.MethodRetryPredicate;
|
||||||
|
import org.springframework.resilience.retry.MethodRetrySpec;
|
||||||
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.util.StringValueResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor
|
||||||
|
* BeanPostProcessor} that applies a retry interceptor to all bean methods
|
||||||
|
* annotated with {@link Retryable} annotations.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
|
||||||
|
implements EmbeddedValueResolverAware {
|
||||||
|
|
||||||
|
private @Nullable StringValueResolver embeddedValueResolver;
|
||||||
|
|
||||||
|
|
||||||
|
public RetryAnnotationBeanPostProcessor() {
|
||||||
|
setBeforeExistingAdvisors(true);
|
||||||
|
|
||||||
|
Pointcut cpc = new AnnotationMatchingPointcut(Retryable.class, true);
|
||||||
|
Pointcut mpc = new AnnotationMatchingPointcut(null, Retryable.class, true);
|
||||||
|
this.advisor = new DefaultPointcutAdvisor(
|
||||||
|
new ComposablePointcut(cpc).union(mpc),
|
||||||
|
new RetryAnnotationInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEmbeddedValueResolver(StringValueResolver resolver) {
|
||||||
|
this.embeddedValueResolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class RetryAnnotationInterceptor extends AbstractRetryInterceptor {
|
||||||
|
|
||||||
|
private final Map<MethodClassKey, MethodRetrySpec> retrySpecCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @Nullable MethodRetrySpec getRetrySpec(Method method, Class<?> targetClass) {
|
||||||
|
MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
|
||||||
|
MethodRetrySpec retrySpec = this.retrySpecCache.get(cacheKey);
|
||||||
|
if (retrySpec != null) {
|
||||||
|
return retrySpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
Retryable retryable = AnnotatedElementUtils.getMergedAnnotation(method, Retryable.class);
|
||||||
|
if (retryable == null) {
|
||||||
|
retryable = AnnotatedElementUtils.getMergedAnnotation(targetClass, Retryable.class);
|
||||||
|
if (retryable == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeUnit timeUnit = retryable.timeUnit();
|
||||||
|
retrySpec = new MethodRetrySpec(
|
||||||
|
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()),
|
||||||
|
instantiatePredicate(retryable.predicate()),
|
||||||
|
parseLong(retryable.maxAttempts(), retryable.maxAttemptsString()),
|
||||||
|
parseDuration(retryable.delay(), retryable.delayString(), timeUnit),
|
||||||
|
parseDuration(retryable.jitter(), retryable.jitterString(), timeUnit),
|
||||||
|
parseDouble(retryable.multiplier(), retryable.multiplierString()),
|
||||||
|
parseDuration(retryable.maxDelay(), retryable.maxDelayString(), timeUnit));
|
||||||
|
|
||||||
|
MethodRetrySpec existing = this.retrySpecCache.putIfAbsent(cacheKey, retrySpec);
|
||||||
|
return (existing != null ? existing : retrySpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) {
|
||||||
|
if (predicateClass == MethodRetryPredicate.class) {
|
||||||
|
return (method, throwable) -> true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (beanFactory != null ? beanFactory.createBean(predicateClass) :
|
||||||
|
ReflectionUtils.accessibleConstructor(predicateClass).newInstance());
|
||||||
|
}
|
||||||
|
catch (Throwable ex) {
|
||||||
|
throw new IllegalStateException("Failed to instantiate predicate class [" + predicateClass + "]", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseLong(long value, String stringValue) {
|
||||||
|
if (StringUtils.hasText(stringValue)) {
|
||||||
|
if (embeddedValueResolver != null) {
|
||||||
|
stringValue = embeddedValueResolver.resolveStringValue(stringValue);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(stringValue)) {
|
||||||
|
return Long.parseLong(stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double parseDouble(double value, String stringValue) {
|
||||||
|
if (StringUtils.hasText(stringValue)) {
|
||||||
|
if (embeddedValueResolver != null) {
|
||||||
|
stringValue = embeddedValueResolver.resolveStringValue(stringValue);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(stringValue)) {
|
||||||
|
return Double.parseDouble(stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Duration parseDuration(long value, String stringValue, TimeUnit timeUnit) {
|
||||||
|
if (StringUtils.hasText(stringValue)) {
|
||||||
|
if (embeddedValueResolver != null) {
|
||||||
|
stringValue = embeddedValueResolver.resolveStringValue(stringValue);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(stringValue)) {
|
||||||
|
return toDuration(stringValue, timeUnit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toDuration(value, timeUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Duration toDuration(long value, TimeUnit timeUnit) {
|
||||||
|
return Duration.of(value, timeUnit.toChronoUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Duration toDuration(String value, TimeUnit timeUnit) {
|
||||||
|
DurationFormat.Unit unit = DurationFormat.Unit.fromChronoUnit(timeUnit.toChronoUnit());
|
||||||
|
return DurationFormatterUtils.detectAndParse(value, unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* 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.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.springframework.aot.hint.annotation.Reflective;
|
||||||
|
import org.springframework.core.annotation.AliasFor;
|
||||||
|
import org.springframework.resilience.retry.MethodRetryPredicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common annotation specifying retry characteristics for an individual method,
|
||||||
|
* or for all proxy-invoked methods in a given class hierarchy if annotated at
|
||||||
|
* the type level.
|
||||||
|
*
|
||||||
|
* <p>Aligned with {@link org.springframework.core.retry.RetryTemplate}
|
||||||
|
* as well as Reactor's retry support, either re-invoking an imperative
|
||||||
|
* target method or decorating a reactive result accordingly.
|
||||||
|
*
|
||||||
|
* <p>Inspired by the <a href="https://github.com/spring-projects/spring-retry">Spring Retry</a>
|
||||||
|
* project but redesigned as a minimal core retry feature in the Spring Framework.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 7.0
|
||||||
|
* @see EnableResilientMethods
|
||||||
|
* @see RetryAnnotationBeanPostProcessor
|
||||||
|
* @see org.springframework.core.retry.RetryPolicy
|
||||||
|
* @see org.springframework.core.retry.RetryTemplate
|
||||||
|
* @see reactor.core.publisher.Mono#retryWhen
|
||||||
|
* @see reactor.core.publisher.Flux#retryWhen
|
||||||
|
*/
|
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Reflective
|
||||||
|
public @interface Retryable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient default attribute for {@link #includes()},
|
||||||
|
* typically used with a single exception type to retry for.
|
||||||
|
*/
|
||||||
|
@AliasFor("includes")
|
||||||
|
Class<? extends Throwable>[] value() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applicable exception types to attempt a retry for. This attribute
|
||||||
|
* allows for the convenient specification of assignable exception types.
|
||||||
|
* <p>The default is empty, leading to a retry attempt for any exception.
|
||||||
|
* @see #excludes()
|
||||||
|
* @see #predicate()
|
||||||
|
*/
|
||||||
|
@AliasFor("value")
|
||||||
|
Class<? extends Throwable>[] includes() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-applicable exception types to avoid a retry for. This attribute
|
||||||
|
* allows for the convenient specification of assignable exception types.
|
||||||
|
* <p>The default is empty, leading to a retry attempt for any exception.
|
||||||
|
* @see #includes()
|
||||||
|
* @see #predicate()
|
||||||
|
*/
|
||||||
|
Class<? extends Throwable>[] excludes() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A predicate for filtering applicable exceptions for which
|
||||||
|
* an invocation can be retried.
|
||||||
|
* <p>The default is a retry attempt for any exception.
|
||||||
|
* <p>A specified {@link MethodRetryPredicate} implementation will be instantiated
|
||||||
|
* per method. It can use dependency injection at the constructor level or through
|
||||||
|
* autowiring annotations, in case it needs access to other beans or facilities.
|
||||||
|
* @see #includes()
|
||||||
|
* @see #excludes()
|
||||||
|
*/
|
||||||
|
Class<? extends MethodRetryPredicate> predicate() default MethodRetryPredicate.class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of retry attempts, in addition to the initial invocation.
|
||||||
|
* <p>The default is 3.
|
||||||
|
*/
|
||||||
|
long maxAttempts() default 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of retry attempts, as a configurable String.
|
||||||
|
* A non-empty value specified here overrides the {@link #maxAttempts()} attribute.
|
||||||
|
* <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions.
|
||||||
|
* @see #maxAttempts()
|
||||||
|
*/
|
||||||
|
String maxAttemptsString() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base delay after the initial invocation. If a multiplier is specified,
|
||||||
|
* this serves as the initial delay to multiply from.
|
||||||
|
* <p>The time unit is milliseconds by default but can be overridden via
|
||||||
|
* {@link #timeUnit}.
|
||||||
|
* <p>The default is 1000.
|
||||||
|
* @see #jitter()
|
||||||
|
* @see #multiplier()
|
||||||
|
* @see #maxDelay()
|
||||||
|
*/
|
||||||
|
long delay() default 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base delay after the initial invocation, as a duration String.
|
||||||
|
* A non-empty value specified here overrides the {@link #delay()} attribute.
|
||||||
|
* <p>The duration String can be in several formats:
|
||||||
|
* <ul>
|
||||||
|
* <li>a plain integer — which is interpreted to represent a duration in
|
||||||
|
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer
|
||||||
|
* using {@link #delay()} in that case)</li>
|
||||||
|
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style
|
||||||
|
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601}
|
||||||
|
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style
|
||||||
|
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li>
|
||||||
|
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li>
|
||||||
|
* </ul>
|
||||||
|
* @return the initial delay as a String value — for example a placeholder,
|
||||||
|
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value
|
||||||
|
* or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value
|
||||||
|
* @see #delay()
|
||||||
|
*/
|
||||||
|
String delayString() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 base {@link #delay()} or
|
||||||
|
* above {@link #maxDelay()}. If a multiplier is specified, it is applied
|
||||||
|
* to the jitter value as well.
|
||||||
|
* <p>The time unit is milliseconds by default but can be overridden via
|
||||||
|
* {@link #timeUnit}.
|
||||||
|
* <p>The default is 0 (no jitter).
|
||||||
|
* @see #delay()
|
||||||
|
* @see #multiplier()
|
||||||
|
* @see #maxDelay()
|
||||||
|
*/
|
||||||
|
long jitter() default 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A jitter value for the base retry attempt, as a duration String.
|
||||||
|
* A non-empty value specified here overrides the {@link #jitter()} attribute.
|
||||||
|
* <p>The duration String can be in several formats:
|
||||||
|
* <ul>
|
||||||
|
* <li>a plain integer — which is interpreted to represent a duration in
|
||||||
|
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer
|
||||||
|
* using {@link #jitter()} in that case)</li>
|
||||||
|
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style
|
||||||
|
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601}
|
||||||
|
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style
|
||||||
|
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li>
|
||||||
|
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li>
|
||||||
|
* </ul>
|
||||||
|
* @return the initial delay as a String value — for example a placeholder,
|
||||||
|
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value
|
||||||
|
* or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value
|
||||||
|
* @see #jitter()
|
||||||
|
*/
|
||||||
|
String jitterString() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A multiplier for a delay for the next retry attempt, applied
|
||||||
|
* to the previous delay (starting with {@link #delay()}) as well
|
||||||
|
* as to the applicable {@link #jitter()} for each attempt.
|
||||||
|
* <p>The default is 1.0, effectively resulting in a fixed delay.
|
||||||
|
* @see #delay()
|
||||||
|
* @see #jitter()
|
||||||
|
* @see #maxDelay()
|
||||||
|
*/
|
||||||
|
double multiplier() default 1.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A multiplier for a delay for the next retry attempt, as a configurable String.
|
||||||
|
* A non-empty value specified here overrides the {@link #multiplier()} attribute.
|
||||||
|
* <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions.
|
||||||
|
* @see #multiplier()
|
||||||
|
*/
|
||||||
|
String multiplierString() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum delay for any retry attempt, limiting how far {@link #jitter()}
|
||||||
|
* and {@link #multiplier()} can increase the {@linkplain #delay() delay}.
|
||||||
|
* <p>The time unit is milliseconds by default but can be overridden via
|
||||||
|
* {@link #timeUnit}.
|
||||||
|
* <p>The default is unlimited.
|
||||||
|
* @see #delay()
|
||||||
|
* @see #jitter()
|
||||||
|
* @see #multiplier()
|
||||||
|
*/
|
||||||
|
long maxDelay() default Long.MAX_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum delay for any retry attempt, as a duration String.
|
||||||
|
* A non-empty value specified here overrides the {@link #maxDelay()} attribute.
|
||||||
|
* <p>The duration String can be in several formats:
|
||||||
|
* <ul>
|
||||||
|
* <li>a plain integer — which is interpreted to represent a duration in
|
||||||
|
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer
|
||||||
|
* using {@link #maxDelay()} in that case)</li>
|
||||||
|
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style
|
||||||
|
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601}
|
||||||
|
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style
|
||||||
|
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li>
|
||||||
|
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li>
|
||||||
|
* </ul>
|
||||||
|
* @return the initial delay as a String value — for example a placeholder,
|
||||||
|
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value
|
||||||
|
* or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value
|
||||||
|
* @see #maxDelay()
|
||||||
|
*/
|
||||||
|
String maxDelayString() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link TimeUnit} to use for {@link #delay}, {@link #delayString},
|
||||||
|
* {@link #jitter}, {@link #jitterString}, {@link #maxDelay}, and
|
||||||
|
* {@link #maxDelayString}.
|
||||||
|
* <p>The default is {@link TimeUnit#MILLISECONDS}.
|
||||||
|
* <p>This attribute is ignored for {@link java.time.Duration} values supplied
|
||||||
|
* via {@link #delayString}, {@link #jitterString}, or {@link #maxDelayString}.
|
||||||
|
* @return the {@code TimeUnit} to use
|
||||||
|
*/
|
||||||
|
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Annotation-based retry and concurrency limit support.
|
||||||
|
*/
|
||||||
|
@NullMarked
|
||||||
|
package org.springframework.resilience.annotation;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.aop.retry;
|
package org.springframework.resilience.retry;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
@ -136,10 +136,7 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||||
|
|
||||||
Publisher<?> publisher = adapter.toPublisher(result);
|
Publisher<?> publisher = adapter.toPublisher(result);
|
||||||
Retry retry = Retry.backoff(spec.maxAttempts(), spec.delay())
|
Retry retry = Retry.backoff(spec.maxAttempts(), spec.delay())
|
||||||
.jitter(
|
.jitter(calculateJitterFactor(spec))
|
||||||
spec.delay().isZero() ? 0.0 :
|
|
||||||
Math.max(0.0, Math.min(1.0, spec.jitter().toNanos() / (double) spec.delay().toNanos()))
|
|
||||||
)
|
|
||||||
.multiplier(spec.multiplier())
|
.multiplier(spec.multiplier())
|
||||||
.maxBackoff(spec.maxDelay())
|
.maxBackoff(spec.maxDelay())
|
||||||
.filter(spec.combinedPredicate().forMethod(method));
|
.filter(spec.combinedPredicate().forMethod(method));
|
||||||
|
@ -147,6 +144,11 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||||
Mono.from(publisher).retryWhen(retry));
|
Mono.from(publisher).retryWhen(retry));
|
||||||
return adapter.fromPublisher(publisher);
|
return adapter.fromPublisher(publisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double calculateJitterFactor(MethodRetrySpec spec) {
|
||||||
|
return (spec.delay().isZero() ? 0.0 :
|
||||||
|
Math.max(0.0, Math.min(1.0, spec.jitter().toNanos() / (double) spec.delay().toNanos())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.aop.retry;
|
package org.springframework.resilience.retry;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.aop.retry;
|
package org.springframework.resilience.retry;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.aop.retry;
|
package org.springframework.resilience.retry;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
* A retry interceptor arrangement based on {@code core.retry} and Reactor.
|
* A retry interceptor arrangement based on {@code core.retry} and Reactor.
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NullMarked
|
||||||
package org.springframework.aop.retry;
|
package org.springframework.resilience.retry;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import org.jspecify.annotations.NullMarked;
|
|
@ -206,14 +206,14 @@ public @interface Scheduled {
|
||||||
* {@link #fixedRate} or {@link #fixedDelay} task.
|
* {@link #fixedRate} or {@link #fixedDelay} task.
|
||||||
* <p>The duration String can be in several formats:
|
* <p>The duration String can be in several formats:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>a plain integer — which is interpreted to represent a duration in
|
* <li>a plain integer — which is interpreted to represent a duration in
|
||||||
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer
|
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer
|
||||||
* using {@link #fixedDelay()} in that case)</li>
|
* using {@link #fixedDelay()} in that case)</li>
|
||||||
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style
|
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style
|
||||||
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601}
|
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601}
|
||||||
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style
|
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style
|
||||||
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li>
|
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li>
|
||||||
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li>
|
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* @return the initial delay as a String value — for example a placeholder,
|
* @return the initial delay as a String value — for example a placeholder,
|
||||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value
|
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value
|
||||||
|
@ -227,7 +227,7 @@ public @interface Scheduled {
|
||||||
* The {@link TimeUnit} to use for {@link #fixedDelay}, {@link #fixedDelayString},
|
* The {@link TimeUnit} to use for {@link #fixedDelay}, {@link #fixedDelayString},
|
||||||
* {@link #fixedRate}, {@link #fixedRateString}, {@link #initialDelay}, and
|
* {@link #fixedRate}, {@link #fixedRateString}, {@link #initialDelay}, and
|
||||||
* {@link #initialDelayString}.
|
* {@link #initialDelayString}.
|
||||||
* <p>Defaults to {@link TimeUnit#MILLISECONDS}.
|
* <p>The default is {@link TimeUnit#MILLISECONDS}.
|
||||||
* <p>This attribute is ignored for {@linkplain #cron() cron expressions}
|
* <p>This attribute is ignored for {@linkplain #cron() cron expressions}
|
||||||
* and for {@link java.time.Duration} values supplied via {@link #fixedDelayString},
|
* and for {@link java.time.Duration} values supplied via {@link #fixedDelayString},
|
||||||
* {@link #fixedRateString}, or {@link #initialDelayString}.
|
* {@link #fixedRateString}, or {@link #initialDelayString}.
|
||||||
|
|
|
@ -552,21 +552,6 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Duration toDuration(long value, TimeUnit timeUnit) {
|
|
||||||
try {
|
|
||||||
return Duration.of(value, timeUnit.toChronoUnit());
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unsupported unit " + timeUnit + " for value \"" + value + "\": " + ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Duration toDuration(String value, TimeUnit timeUnit) {
|
|
||||||
DurationFormat.Unit unit = DurationFormat.Unit.fromChronoUnit(timeUnit.toChronoUnit());
|
|
||||||
return DurationFormatterUtils.detectAndParse(value, unit); // interpreting as long as fallback already
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all currently scheduled tasks, from {@link Scheduled} methods
|
* Return all currently scheduled tasks, from {@link Scheduled} methods
|
||||||
* as well as from programmatic {@link SchedulingConfigurer} interaction.
|
* as well as from programmatic {@link SchedulingConfigurer} interaction.
|
||||||
|
@ -669,4 +654,14 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Duration toDuration(long value, TimeUnit timeUnit) {
|
||||||
|
return Duration.of(value, timeUnit.toChronoUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Duration toDuration(String value, TimeUnit timeUnit) {
|
||||||
|
DurationFormat.Unit unit = DurationFormat.Unit.fromChronoUnit(timeUnit.toChronoUnit());
|
||||||
|
return DurationFormatterUtils.detectAndParse(value, unit);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
* 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.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.aop.framework.AopProxyUtils;
|
||||||
|
import org.springframework.aop.framework.ProxyFactory;
|
||||||
|
import org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor;
|
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
|
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||||
|
import org.springframework.resilience.annotation.ConcurrencyLimit;
|
||||||
|
import org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcessor;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public class ConcurrencyLimitTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withSimpleInterceptor() {
|
||||||
|
NonAnnotatedBean target = new NonAnnotatedBean();
|
||||||
|
ProxyFactory pf = new ProxyFactory();
|
||||||
|
pf.setTarget(target);
|
||||||
|
pf.addAdvice(new ConcurrencyThrottleInterceptor(2));
|
||||||
|
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy();
|
||||||
|
|
||||||
|
List<CompletableFuture<?>> futures = new ArrayList<>(10);
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
futures.add(CompletableFuture.runAsync(proxy::concurrentOperation));
|
||||||
|
}
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
assertThat(target.counter).hasValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withPostProcessorForMethod() {
|
||||||
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||||
|
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class));
|
||||||
|
ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor();
|
||||||
|
bpp.setBeanFactory(bf);
|
||||||
|
bf.addBeanPostProcessor(bpp);
|
||||||
|
AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class);
|
||||||
|
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||||
|
|
||||||
|
List<CompletableFuture<?>> futures = new ArrayList<>(10);
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
futures.add(CompletableFuture.runAsync(proxy::concurrentOperation));
|
||||||
|
}
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
assertThat(target.current).hasValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withPostProcessorForClass() {
|
||||||
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||||
|
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
|
||||||
|
ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor();
|
||||||
|
bpp.setBeanFactory(bf);
|
||||||
|
bf.addBeanPostProcessor(bpp);
|
||||||
|
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class);
|
||||||
|
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||||
|
|
||||||
|
List<CompletableFuture<?>> futures = new ArrayList<>(30);
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
futures.add(CompletableFuture.runAsync(proxy::concurrentOperation));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
futures.add(CompletableFuture.runAsync(proxy::otherOperation));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
futures.add(CompletableFuture.runAsync(proxy::overrideOperation));
|
||||||
|
}
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
assertThat(target.current).hasValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class NonAnnotatedBean {
|
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
|
public void concurrentOperation() {
|
||||||
|
if (counter.incrementAndGet() > 2) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
counter.decrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class AnnotatedMethodBean {
|
||||||
|
|
||||||
|
AtomicInteger current = new AtomicInteger();
|
||||||
|
|
||||||
|
@ConcurrencyLimit(2)
|
||||||
|
public void concurrentOperation() {
|
||||||
|
if (current.incrementAndGet() > 2) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
current.decrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ConcurrencyLimit(2)
|
||||||
|
public static class AnnotatedClassBean {
|
||||||
|
|
||||||
|
AtomicInteger current = new AtomicInteger();
|
||||||
|
|
||||||
|
AtomicInteger currentOverride = new AtomicInteger();
|
||||||
|
|
||||||
|
public void concurrentOperation() {
|
||||||
|
if (current.incrementAndGet() > 2) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
current.decrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void otherOperation() {
|
||||||
|
if (current.incrementAndGet() > 2) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
current.decrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConcurrencyLimit(1)
|
||||||
|
public void overrideOperation() {
|
||||||
|
if (currentOverride.incrementAndGet() > 1) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
currentOverride.decrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.aop.retry;
|
package org.springframework.resilience;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
@ -28,11 +28,13 @@ import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.aop.framework.AopProxyUtils;
|
import org.springframework.aop.framework.AopProxyUtils;
|
||||||
import org.springframework.aop.framework.ProxyFactory;
|
import org.springframework.aop.framework.ProxyFactory;
|
||||||
import org.springframework.aop.retry.annotation.RetryAnnotationBeanPostProcessor;
|
|
||||||
import org.springframework.aop.retry.annotation.RetryAnnotationInterceptor;
|
|
||||||
import org.springframework.aop.retry.annotation.Retryable;
|
|
||||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
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.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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
@ -58,19 +60,6 @@ public class ReactiveRetryInterceptorTests {
|
||||||
assertThat(target.counter.get()).isEqualTo(6);
|
assertThat(target.counter.get()).isEqualTo(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void withAnnotationInterceptorForMethod() {
|
|
||||||
AnnotatedMethodBean target = new AnnotatedMethodBean();
|
|
||||||
ProxyFactory pf = new ProxyFactory();
|
|
||||||
pf.setTarget(target);
|
|
||||||
pf.addAdvice(new RetryAnnotationInterceptor());
|
|
||||||
AnnotatedMethodBean proxy = (AnnotatedMethodBean) pf.getProxy();
|
|
||||||
|
|
||||||
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())
|
|
||||||
.withCauseInstanceOf(IOException.class).havingCause().withMessage("6");
|
|
||||||
assertThat(target.counter.get()).isEqualTo(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void withPostProcessorForMethod() {
|
void withPostProcessorForMethod() {
|
||||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||||
|
@ -86,25 +75,6 @@ public class ReactiveRetryInterceptorTests {
|
||||||
assertThat(target.counter.get()).isEqualTo(6);
|
assertThat(target.counter.get()).isEqualTo(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void withAnnotationInterceptorForClass() {
|
|
||||||
AnnotatedClassBean target = new AnnotatedClassBean();
|
|
||||||
ProxyFactory pf = new ProxyFactory();
|
|
||||||
pf.setTarget(target);
|
|
||||||
pf.addAdvice(new RetryAnnotationInterceptor());
|
|
||||||
AnnotatedClassBean proxy = (AnnotatedClassBean) pf.getProxy();
|
|
||||||
|
|
||||||
assertThatRuntimeException().isThrownBy(() -> proxy.retryOperation().block())
|
|
||||||
.withCauseInstanceOf(IOException.class).havingCause().withMessage("3");
|
|
||||||
assertThat(target.counter.get()).isEqualTo(3);
|
|
||||||
assertThatRuntimeException().isThrownBy(() -> proxy.otherOperation().block())
|
|
||||||
.withCauseInstanceOf(IOException.class);
|
|
||||||
assertThat(target.counter.get()).isEqualTo(4);
|
|
||||||
assertThatIllegalStateException().isThrownBy(() -> proxy.overrideOperation().blockFirst())
|
|
||||||
.withCauseInstanceOf(IOException.class);
|
|
||||||
assertThat(target.counter.get()).isEqualTo(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void withPostProcessorForClass() {
|
void withPostProcessorForClass() {
|
||||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||||
|
@ -218,6 +188,7 @@ public class ReactiveRetryInterceptorTests {
|
||||||
assertThat(target.counter.get()).isEqualTo(4);
|
assertThat(target.counter.get()).isEqualTo(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class NonAnnotatedBean {
|
public static class NonAnnotatedBean {
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
@ -284,8 +255,11 @@ public class ReactiveRetryInterceptorTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Bean classes for boundary testing
|
// Bean classes for boundary testing
|
||||||
|
|
||||||
public static class MinimalRetryBean {
|
public static class MinimalRetryBean {
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
public Mono<Object> retryOperation() {
|
public Mono<Object> retryOperation() {
|
||||||
|
@ -296,7 +270,9 @@ public class ReactiveRetryInterceptorTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class ZeroDelayJitterBean {
|
public static class ZeroDelayJitterBean {
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
public Mono<Object> retryOperation() {
|
public Mono<Object> retryOperation() {
|
||||||
|
@ -307,7 +283,9 @@ public class ReactiveRetryInterceptorTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class JitterGreaterThanDelayBean {
|
public static class JitterGreaterThanDelayBean {
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
public Mono<Object> retryOperation() {
|
public Mono<Object> retryOperation() {
|
||||||
|
@ -318,7 +296,9 @@ public class ReactiveRetryInterceptorTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class FluxMultiValueBean {
|
public static class FluxMultiValueBean {
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
public Flux<Object> retryOperation() {
|
public Flux<Object> retryOperation() {
|
||||||
|
@ -329,7 +309,9 @@ public class ReactiveRetryInterceptorTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class SuccessfulOperationBean {
|
public static class SuccessfulOperationBean {
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
public Mono<String> retryOperation() {
|
public Mono<String> retryOperation() {
|
||||||
|
@ -340,7 +322,9 @@ public class ReactiveRetryInterceptorTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class ImmediateFailureBean {
|
public static class ImmediateFailureBean {
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
public Mono<Object> retryOperation() {
|
public Mono<Object> retryOperation() {
|
|
@ -14,22 +14,31 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.aop.retry;
|
package org.springframework.resilience;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
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.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.aop.framework.AopProxyUtils;
|
import org.springframework.aop.framework.AopProxyUtils;
|
||||||
import org.springframework.aop.framework.ProxyFactory;
|
import org.springframework.aop.framework.ProxyFactory;
|
||||||
import org.springframework.aop.retry.annotation.RetryAnnotationBeanPostProcessor;
|
|
||||||
import org.springframework.aop.retry.annotation.RetryAnnotationInterceptor;
|
|
||||||
import org.springframework.aop.retry.annotation.Retryable;
|
|
||||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
|
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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIOException;
|
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||||
|
@ -53,18 +62,6 @@ public class RetryInterceptorTests {
|
||||||
assertThat(target.counter).isEqualTo(6);
|
assertThat(target.counter).isEqualTo(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void withAnnotationInterceptorForMethod() {
|
|
||||||
AnnotatedMethodBean target = new AnnotatedMethodBean();
|
|
||||||
ProxyFactory pf = new ProxyFactory();
|
|
||||||
pf.setTarget(target);
|
|
||||||
pf.addAdvice(new RetryAnnotationInterceptor());
|
|
||||||
AnnotatedMethodBean proxy = (AnnotatedMethodBean) pf.getProxy();
|
|
||||||
|
|
||||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
|
||||||
assertThat(target.counter).isEqualTo(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void withPostProcessorForMethod() {
|
void withPostProcessorForMethod() {
|
||||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||||
|
@ -79,22 +76,6 @@ public class RetryInterceptorTests {
|
||||||
assertThat(target.counter).isEqualTo(6);
|
assertThat(target.counter).isEqualTo(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void withAnnotationInterceptorForClass() {
|
|
||||||
AnnotatedClassBean target = new AnnotatedClassBean();
|
|
||||||
ProxyFactory pf = new ProxyFactory();
|
|
||||||
pf.setTarget(target);
|
|
||||||
pf.addAdvice(new RetryAnnotationInterceptor());
|
|
||||||
AnnotatedClassBean proxy = (AnnotatedClassBean) pf.getProxy();
|
|
||||||
|
|
||||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3");
|
|
||||||
assertThat(target.counter).isEqualTo(3);
|
|
||||||
assertThatIOException().isThrownBy(proxy::otherOperation);
|
|
||||||
assertThat(target.counter).isEqualTo(4);
|
|
||||||
assertThatIOException().isThrownBy(proxy::overrideOperation);
|
|
||||||
assertThat(target.counter).isEqualTo(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void withPostProcessorForClass() {
|
void withPostProcessorForClass() {
|
||||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||||
|
@ -113,6 +94,48 @@ public class RetryInterceptorTests {
|
||||||
assertThat(target.counter).isEqualTo(6);
|
assertThat(target.counter).isEqualTo(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withPostProcessorForClassWithStrings() {
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.setProperty("delay", "10");
|
||||||
|
props.setProperty("jitter", "5");
|
||||||
|
props.setProperty("multiplier", "2.0");
|
||||||
|
props.setProperty("maxDelay", "40");
|
||||||
|
props.setProperty("limitedAttempts", "1");
|
||||||
|
|
||||||
|
GenericApplicationContext ctx = new GenericApplicationContext();
|
||||||
|
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("props", props));
|
||||||
|
ctx.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBeanWithStrings.class));
|
||||||
|
ctx.registerBeanDefinition("bpp", new RootBeanDefinition(RetryAnnotationBeanPostProcessor.class));
|
||||||
|
ctx.refresh();
|
||||||
|
AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class);
|
||||||
|
AnnotatedClassBeanWithStrings target = (AnnotatedClassBeanWithStrings) AopProxyUtils.getSingletonTarget(proxy);
|
||||||
|
|
||||||
|
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3");
|
||||||
|
assertThat(target.counter).isEqualTo(3);
|
||||||
|
assertThatIOException().isThrownBy(proxy::otherOperation);
|
||||||
|
assertThat(target.counter).isEqualTo(4);
|
||||||
|
assertThatIOException().isThrownBy(proxy::overrideOperation);
|
||||||
|
assertThat(target.counter).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withEnableAnnotation() throws Exception {
|
||||||
|
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||||
|
ctx.registerBeanDefinition("bean", new RootBeanDefinition(DoubleAnnotatedBean.class));
|
||||||
|
ctx.registerBeanDefinition("config", new RootBeanDefinition(EnablingConfig.class));
|
||||||
|
ctx.refresh();
|
||||||
|
DoubleAnnotatedBean proxy = ctx.getBean(DoubleAnnotatedBean.class);
|
||||||
|
DoubleAnnotatedBean target = (DoubleAnnotatedBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||||
|
|
||||||
|
Thread thread = new Thread(() -> assertThatIOException().isThrownBy(proxy::retryOperation));
|
||||||
|
thread.start();
|
||||||
|
assertThatIOException().isThrownBy(proxy::retryOperation);
|
||||||
|
thread.join();
|
||||||
|
assertThat(target.counter).hasValue(6);
|
||||||
|
assertThat(target.threadChange).hasValue(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class NonAnnotatedBean {
|
public static class NonAnnotatedBean {
|
||||||
|
|
||||||
|
@ -162,6 +185,32 @@ public class RetryInterceptorTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Retryable(delayString = "${delay}", jitterString = "${jitter}",
|
||||||
|
multiplierString = "${multiplier}", maxDelayString = "${maxDelay}",
|
||||||
|
includes = IOException.class, excludes = AccessDeniedException.class,
|
||||||
|
predicate = CustomPredicate.class)
|
||||||
|
public static class AnnotatedClassBeanWithStrings {
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
|
|
||||||
|
public void retryOperation() throws IOException {
|
||||||
|
counter++;
|
||||||
|
throw new IOException(Integer.toString(counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void otherOperation() throws IOException {
|
||||||
|
counter++;
|
||||||
|
throw new AccessDeniedException(Integer.toString(counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retryable(value = IOException.class, maxAttemptsString = "${limitedAttempts}", delayString = "10ms")
|
||||||
|
public void overrideOperation() throws IOException {
|
||||||
|
counter++;
|
||||||
|
throw new AccessDeniedException(Integer.toString(counter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static class CustomPredicate implements MethodRetryPredicate {
|
private static class CustomPredicate implements MethodRetryPredicate {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -170,4 +219,36 @@ public class RetryInterceptorTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class DoubleAnnotatedBean {
|
||||||
|
|
||||||
|
AtomicInteger current = new AtomicInteger();
|
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
|
AtomicInteger threadChange = new AtomicInteger();
|
||||||
|
|
||||||
|
volatile String lastThreadName;
|
||||||
|
|
||||||
|
@ConcurrencyLimit(1)
|
||||||
|
@Retryable(maxAttempts = 2, delay = 10)
|
||||||
|
public void retryOperation() throws IOException, InterruptedException {
|
||||||
|
if (current.incrementAndGet() > 1) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
Thread.sleep(100);
|
||||||
|
current.decrementAndGet();
|
||||||
|
if (!Thread.currentThread().getName().equals(lastThreadName)) {
|
||||||
|
lastThreadName = Thread.currentThread().getName();
|
||||||
|
threadChange.incrementAndGet();
|
||||||
|
}
|
||||||
|
throw new IOException(Integer.toString(counter.incrementAndGet()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EnableResilientMethods
|
||||||
|
public static class EnablingConfig {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue