Files
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

34 lines
2.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.