Files
spring-boot-demo/resilience/docs/04-concurrency-limit.md
T
asmhatreandClaude Opus 5 7e1676c763 Add resilience: Spring Framework 7 @Retryable and @ConcurrencyLimit
Companion code for "Spring Framework 7's Built-in Resilience: @Retryable,
@ConcurrencyLimit, and What's Left for Resilience4j". Every retry counted
by recording real invocations: defaults, backoff and jitter, timeout,
reactive and CompletableFuture returns, the concurrency limit's BLOCK and
REJECT policies, retries around transactions, composition with
Resilience4j 2.4.0, and the annotation API across 7.0.0-7.0.9.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
2026-09-11 17:12:25 +00:00

2.1 KiB
Raw Blame History

4. @ConcurrencyLimit: BLOCK, REJECT, and nesting with retry

← 3. @Retryable measured · Index · Next: 5. Retry and transactions →

Ten virtual threads call a 200 ms method limited to 2 (ReportService):

Setup Succeeded Failed Max inside Elapsed Transcript
no limit 10 - 10 203 ms limit-none.txt
@ConcurrencyLimit(2) (BLOCK) 10 - 2 1006 ms limit-block.txt
policy = REJECT 2 8 × InvocationRejectedException 2 205 ms limit-reject.txt
Resilience4j @Bulkhead, max-wait-duration: 0 2 8 × BulkheadFullException 2 231 ms limit-r4j-bulkhead.txt

BLOCK queues callers indefinitely - there is no wait timeout. With virtual threads that is cheap in memory, but an unbounded queue in front of a slow dependency is still an unbounded latency. InvocationRejectedException extends RejectedExecutionException, so existing handlers for executor rejection catch it.

Both annotations on one method

limitedAndRetried() has @ConcurrencyLimit(1) and @Retryable(maxRetries=1, delay=300); every caller's first attempt fails. Two callers took 815 ms (limit-limit-and-retry.txt): about 2 × (50 + 300 + 50). The permit is held through the 300 ms back-off, because the limit is the outer interceptor (proxy-reports.txt):

"org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcessor$ConcurrencyLimitInterceptor  order=2147483647",
"org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor$RetryAnnotationInterceptor  order=2147483647"

Both post-processors insert at the front of an existing proxy, so whichever runs second ends up outermost. If you want the permit released between attempts, put the two annotations on different beans.