Skip to content

Commit 5ac3c40

Browse files
committed
Prevent empty declaration of @⁠ConcurrencyLimit
As a follow-up to gh-35461 and a comment left on the Spring Blog, we have decided to prevent empty declarations of @⁠ConcurrencyLimit, thereby requiring users to explicitly declare the value for the limit. Closes gh-35523
1 parent 8b254ad commit 5ac3c40

File tree

3 files changed

+110
-30
lines changed

3 files changed

+110
-30
lines changed

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,30 @@
6464
* @see #limitString()
6565
*/
6666
@AliasFor("limit")
67-
int value() default 1;
67+
int value() default Integer.MIN_VALUE;
6868

6969
/**
70-
* The applicable concurrency limit: 1 by default,
71-
* effectively locking the target instance for each method invocation.
72-
* <p>Specify a limit higher than 1 for pool-like throttling, constraining
70+
* The concurrency limit.
71+
* <p>Specify {@code 1} to effectively lock the target instance for each method
72+
* invocation.
73+
* <p>Specify a limit greater than {@code 1} for pool-like throttling, constraining
7374
* the number of concurrent invocations similar to the upper bound of a pool.
75+
* <p>Specify {@code -1} for unbounded concurrency.
7476
* @see #value()
7577
* @see #limitString()
78+
* @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY
7679
*/
7780
@AliasFor("value")
78-
int limit() default 1;
81+
int limit() default Integer.MIN_VALUE;
7982

8083
/**
8184
* The concurrency limit, as a configurable String.
82-
* <p>A non-empty value specified here overrides the {@link #limit()} (or
83-
* {@link #value()}) attribute.
85+
* <p>A non-empty value specified here overrides the {@link #limit()} and
86+
* {@link #value()} attributes.
8487
* <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions.
88+
* <p>See the Javadoc for {@link #limit()} for details on supported values.
8589
* @see #limit()
90+
* @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY
8691
*/
8792
String limitString() default "";
8893

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ private class ConcurrencyLimitInterceptor implements MethodInterceptor {
108108
if (interceptor == null) {
109109
Assert.state(annotation != null, "No @ConcurrencyLimit annotation found");
110110
int concurrencyLimit = parseInt(annotation.limit(), annotation.limitString());
111+
if (concurrencyLimit < -1) {
112+
throw new IllegalStateException(annotation + " must be configured with a valid limit");
113+
}
111114
interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit);
112115
if (!perMethod) {
113116
cache.classInterceptor = interceptor;

spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@
3535
import org.springframework.resilience.annotation.EnableResilientMethods;
3636

3737
import static org.assertj.core.api.Assertions.assertThat;
38+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
39+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3840

3941
/**
4042
* @author Juergen Hoeller
4143
* @author Hyunsang Han
44+
* @author Sam Brannen
4245
* @since 7.0
4346
*/
4447
class ConcurrencyLimitTests {
@@ -61,12 +64,7 @@ void withSimpleInterceptor() {
6164

6265
@Test
6366
void withPostProcessorForMethod() {
64-
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
65-
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class));
66-
ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor();
67-
bpp.setBeanFactory(bf);
68-
bf.addBeanPostProcessor(bpp);
69-
AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class);
67+
AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class);
7068
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
7169

7270
List<CompletableFuture<?>> futures = new ArrayList<>(10);
@@ -77,14 +75,22 @@ void withPostProcessorForMethod() {
7775
assertThat(target.current).hasValue(0);
7876
}
7977

78+
@Test
79+
void withPostProcessorForMethodWithUnboundedConcurrency() {
80+
AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class);
81+
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
82+
83+
List<CompletableFuture<?>> futures = new ArrayList<>(10);
84+
for (int i = 0; i < 10; i++) {
85+
futures.add(CompletableFuture.runAsync(proxy::unboundedConcurrency));
86+
}
87+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
88+
assertThat(target.current).hasValue(10);
89+
}
90+
8091
@Test
8192
void withPostProcessorForClass() {
82-
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
83-
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
84-
ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor();
85-
bpp.setBeanFactory(bf);
86-
bf.addBeanPostProcessor(bpp);
87-
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class);
93+
AnnotatedClassBean proxy = createProxy(AnnotatedClassBean.class);
8894
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
8995

9096
List<CompletableFuture<?>> futures = new ArrayList<>(30);
@@ -122,17 +128,52 @@ void withPlaceholderResolution() {
122128
ctx.close();
123129
}
124130

131+
@Test
132+
void configurationErrors() {
133+
ConfigurationErrorsBean proxy = createProxy(ConfigurationErrorsBean.class);
134+
135+
assertThatIllegalStateException()
136+
.isThrownBy(proxy::emptyDeclaration)
137+
.withMessageMatching("@.+?ConcurrencyLimit(.+?) must be configured with a valid limit")
138+
.withMessageContaining("\"\"")
139+
.withMessageContaining(String.valueOf(Integer.MIN_VALUE));
140+
141+
assertThatIllegalStateException()
142+
.isThrownBy(proxy::negative42Int)
143+
.withMessageMatching("@.+?ConcurrencyLimit(.+?) must be configured with a valid limit")
144+
.withMessageContaining("-42");
145+
146+
assertThatIllegalStateException()
147+
.isThrownBy(proxy::negative42String)
148+
.withMessageMatching("@.+?ConcurrencyLimit(.+?) must be configured with a valid limit")
149+
.withMessageContaining("-42");
150+
151+
assertThatExceptionOfType(NumberFormatException.class)
152+
.isThrownBy(proxy::alphanumericString)
153+
.withMessageContaining("B2");
154+
}
155+
156+
157+
private static <T> T createProxy(Class<T> beanClass) {
158+
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
159+
bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass));
160+
ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor();
161+
bpp.setBeanFactory(bf);
162+
bf.addBeanPostProcessor(bpp);
163+
return bf.getBean(beanClass);
164+
}
165+
125166

126167
static class NonAnnotatedBean {
127168

128-
AtomicInteger counter = new AtomicInteger();
169+
final AtomicInteger counter = new AtomicInteger();
129170

130171
public void concurrentOperation() {
131172
if (counter.incrementAndGet() > 2) {
132173
throw new IllegalStateException();
133174
}
134175
try {
135-
Thread.sleep(100);
176+
Thread.sleep(10);
136177
}
137178
catch (InterruptedException ex) {
138179
throw new IllegalStateException(ex);
@@ -144,37 +185,48 @@ public void concurrentOperation() {
144185

145186
static class AnnotatedMethodBean {
146187

147-
AtomicInteger current = new AtomicInteger();
188+
final AtomicInteger current = new AtomicInteger();
148189

149190
@ConcurrencyLimit(2)
150191
public void concurrentOperation() {
151192
if (current.incrementAndGet() > 2) {
152193
throw new IllegalStateException();
153194
}
154195
try {
155-
Thread.sleep(100);
196+
Thread.sleep(10);
156197
}
157198
catch (InterruptedException ex) {
158199
throw new IllegalStateException(ex);
159200
}
160201
current.decrementAndGet();
161202
}
203+
204+
@ConcurrencyLimit(limit = -1)
205+
public void unboundedConcurrency() {
206+
current.incrementAndGet();
207+
try {
208+
Thread.sleep(10);
209+
}
210+
catch (InterruptedException ex) {
211+
throw new IllegalStateException(ex);
212+
}
213+
}
162214
}
163215

164216

165217
@ConcurrencyLimit(2)
166218
static class AnnotatedClassBean {
167219

168-
AtomicInteger current = new AtomicInteger();
220+
final AtomicInteger current = new AtomicInteger();
169221

170-
AtomicInteger currentOverride = new AtomicInteger();
222+
final AtomicInteger currentOverride = new AtomicInteger();
171223

172224
public void concurrentOperation() {
173225
if (current.incrementAndGet() > 2) {
174226
throw new IllegalStateException();
175227
}
176228
try {
177-
Thread.sleep(100);
229+
Thread.sleep(10);
178230
}
179231
catch (InterruptedException ex) {
180232
throw new IllegalStateException(ex);
@@ -187,7 +239,7 @@ public void otherOperation() {
187239
throw new IllegalStateException();
188240
}
189241
try {
190-
Thread.sleep(100);
242+
Thread.sleep(10);
191243
}
192244
catch (InterruptedException ex) {
193245
throw new IllegalStateException(ex);
@@ -201,7 +253,7 @@ public void overrideOperation() {
201253
throw new IllegalStateException();
202254
}
203255
try {
204-
Thread.sleep(100);
256+
Thread.sleep(10);
205257
}
206258
catch (InterruptedException ex) {
207259
throw new IllegalStateException(ex);
@@ -218,15 +270,15 @@ static class PlaceholderTestConfig {
218270

219271
static class PlaceholderBean {
220272

221-
AtomicInteger current = new AtomicInteger();
273+
final AtomicInteger current = new AtomicInteger();
222274

223275
@ConcurrencyLimit(limitString = "${test.concurrency.limit}")
224276
public void concurrentOperation() {
225277
if (current.incrementAndGet() > 3) { // Assumes test.concurrency.limit=3
226278
throw new IllegalStateException();
227279
}
228280
try {
229-
Thread.sleep(100);
281+
Thread.sleep(10);
230282
}
231283
catch (InterruptedException ex) {
232284
throw new IllegalStateException(ex);
@@ -235,4 +287,24 @@ public void concurrentOperation() {
235287
}
236288
}
237289

290+
291+
static class ConfigurationErrorsBean {
292+
293+
@ConcurrencyLimit
294+
public void emptyDeclaration() {
295+
}
296+
297+
@ConcurrencyLimit(-42)
298+
public void negative42Int() {
299+
}
300+
301+
@ConcurrencyLimit(limitString = "-42")
302+
public void negative42String() {
303+
}
304+
305+
@ConcurrencyLimit(limitString = "B2")
306+
public void alphanumericString() {
307+
}
308+
}
309+
238310
}

0 commit comments

Comments
 (0)