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
2.1 KiB
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.