Add configurable throttle policy to @ConcurrencyLimit annotation

Closes gh-36109
This commit is contained in:
Juergen Hoeller
2026-01-08 15:50:57 +01:00
parent 644072f7ea
commit 38f5f4de8e
5 changed files with 234 additions and 8 deletions
@@ -0,0 +1,55 @@
/*
* 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.concurrent.RejectedExecutionException;
/**
* Exception thrown when a target will not get invoked due to a resilience policy,
* such as the concurrency limit having been reached for a class/method annotated with
* {@link org.springframework.resilience.annotation.ConcurrencyLimit @ConcurrencyLimit}.
*
* @author Juergen Hoeller
* @since 7.0.3
* @see org.springframework.resilience.annotation.ConcurrencyLimit#rejectOnExcess()
*/
@SuppressWarnings("serial")
public class InvocationRejectedException extends RejectedExecutionException {
private final Object target;
/**
* Create a new {@code InvocationRejectedException}
* with the specified detail message and target instance.
* @param msg the detail message
* @param target the target instance that was about to be invoked
*/
public InvocationRejectedException(String msg, Object target) {
super(msg);
this.target = target;
}
/**
* Return the target instance that was about to be invoked.
*/
public Object getTarget() {
return this.target;
}
}
@@ -28,7 +28,9 @@ import org.springframework.core.annotation.AliasFor;
/**
* 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.
* the type level. The default behavior is to block further method invocations
* when the limit has been reached. Alternatively, further invocations can be
* rejected through configuring {@link #policy()} as {@code policy = REJECT}.
*
* <p>In the type-level case, all methods inheriting the concurrency limit
* from the type level share a common concurrency throttle, with any mix
@@ -95,4 +97,35 @@ public @interface ConcurrencyLimit {
*/
String limitString() default "";
/**
* The policy for throttling method invocations when the limit has been reached.
* <p>The default behavior is to block further concurrent invocations once the
* specified limit has been reached: {@link ThrottlePolicy#BLOCK}.
* <p>Switch this policy to {@code REJECT} for rejecting further invocations instead,
* throwing {@link org.springframework.resilience.InvocationRejectedException}
* (which extends the common {@link java.util.concurrent.RejectedExecutionException})
* on any further concurrent invocation attempts: {@link ThrottlePolicy#REJECT}.
* @since 7.0.3
*/
ThrottlePolicy policy() default ThrottlePolicy.BLOCK;
/**
* Policy to apply for throttling method invocations when the limit has been reached.
* @since 7.0.3
*/
enum ThrottlePolicy {
/**
* The default: block until we can invoke the method within the configured limit.
*/
BLOCK,
/**
* Alternative: reject further method invocations once the limit has been reached.
* @see org.springframework.resilience.InvocationRejectedException
*/
REJECT
}
}
@@ -35,7 +35,9 @@ import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.resilience.InvocationRejectedException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
@@ -115,7 +117,11 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA
if (concurrencyLimit < -1) {
throw new IllegalStateException(annotation + " must be configured with a valid limit");
}
interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit);
interceptor = (annotation.policy() == ConcurrencyLimit.ThrottlePolicy.REJECT ?
new RejectingConcurrencyThrottleInterceptor(concurrencyLimit,
(perMethod ? ClassUtils.getQualifiedMethodName(method) : targetClass.getName()),
instance) :
new ConcurrencyThrottleInterceptor(concurrencyLimit));
if (!perMethod) {
holder.classInterceptor = interceptor;
}
@@ -148,4 +154,24 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA
@Nullable MethodInterceptor classInterceptor;
}
private static class RejectingConcurrencyThrottleInterceptor extends ConcurrencyThrottleInterceptor {
private final String identifier;
private final Object target;
public RejectingConcurrencyThrottleInterceptor(int concurrencyLimit, String identifier, Object target) {
super(concurrencyLimit);
this.identifier = identifier;
this.target = target;
}
@Override
protected void onLimitReached() {
throw new InvocationRejectedException(
"Concurrency limit reached for " + this.identifier + ": " + getConcurrencyLimit(), this.target);
}
}
}
@@ -0,0 +1,7 @@
/**
* Common exceptions thrown by Spring's resilience facilities.
*/
@NullMarked
package org.springframework.resilience;
import org.jspecify.annotations.NullMarked;