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
34 lines
2.1 KiB
Markdown
34 lines
2.1 KiB
Markdown
# 4. `@ConcurrencyLimit`: BLOCK, REJECT, and nesting with retry
|
||
|
||
[← 3. @Retryable measured](03-retryable-measured.md) · [Index](../README.md) · Next: [5. Retry and transactions →](05-retry-and-transactions.md)
|
||
|
||
Ten virtual threads call a 200 ms method limited to 2 ([`ReportService`](../src/main/java/com/ankurm/resilience/limit/ReportService.java)):
|
||
|
||
| Setup | Succeeded | Failed | Max inside | Elapsed | Transcript |
|
||
|---|---|---|---|---|---|
|
||
| no limit | 10 | - | 10 | 203 ms | [`limit-none.txt`](output/limit-none.txt) |
|
||
| `@ConcurrencyLimit(2)` (BLOCK) | 10 | - | 2 | 1006 ms | [`limit-block.txt`](output/limit-block.txt) |
|
||
| `policy = REJECT` | 2 | 8 × `InvocationRejectedException` | 2 | 205 ms | [`limit-reject.txt`](output/limit-reject.txt) |
|
||
| Resilience4j `@Bulkhead`, `max-wait-duration: 0` | 2 | 8 × `BulkheadFullException` | 2 | 231 ms | [`limit-r4j-bulkhead.txt`](output/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`](output/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`](output/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.
|