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
69 lines
3.5 KiB
Markdown
69 lines
3.5 KiB
Markdown
# 6. What is left for Resilience4j
|
|
|
|
[← 5. Retry and transactions](05-retry-and-transactions.md) · [Index](../README.md) · Next: [7. Migrating from spring-retry →](07-migrating-from-spring-retry.md)
|
|
|
|
| Need | Spring Framework 7.0.9 | Resilience4j 2.4.0 |
|
|
|---|---|---|
|
|
| Retry with back-off and jitter | `@Retryable`, `RetryTemplate` | `@Retry` |
|
|
| Limit concurrent calls | `@ConcurrencyLimit` (BLOCK/REJECT) | `@Bulkhead` (semaphore or thread pool, with a wait timeout) |
|
|
| Circuit breaker | - | `@CircuitBreaker` |
|
|
| Rate limiter (calls per period) | - | `@RateLimiter` |
|
|
| Time limit on a call | - (`timeout` is a retry budget) | `@TimeLimiter` (async return types) |
|
|
| Declarative fallback / recovery | - (`@Recover` request declined, spring-framework#35685) | `fallbackMethod` |
|
|
| Metrics | none built in | `resilience4j.*` meters, Actuator endpoints, health |
|
|
| Per-instance config in properties | placeholders in `*String` attributes | `resilience4j.<module>.instances.<name>.*` |
|
|
|
|
## The breaker, measured
|
|
|
|
`sliding-window-size: 10`, `minimum-number-of-calls: 5`, `failure-rate-threshold: 50`, 2 s open
|
|
([`r4j-breaker.txt`](output/r4j-breaker.txt)): five failures reach the downstream, the breaker
|
|
opens, calls 6-8 fail in microseconds with `CallNotPermittedException`, and after 2.1 s the first
|
|
call goes through (`HALF_OPEN`), the second closes it.
|
|
|
|
**Resilience4j's own defaults are `slidingWindowSize=100` and `minimumNumberOfCalls=100`** (read
|
|
from `CircuitBreakerConfig.ofDefaults()`), with 60 s open. A breaker left on defaults cannot open
|
|
until it has seen a hundred calls - in a low-traffic service, effectively never.
|
|
|
|
## `maxAttempts` vs `maxRetries`
|
|
|
|
[`r4j-retry.txt`](output/r4j-retry.txt): `max-attempts: 3` → **3** invocations. Spring's
|
|
`maxRetries = 3` → **4**. Same number, different meaning.
|
|
|
|
## Both libraries on one method
|
|
|
|
`chargeWithBoth()` carries `@CircuitBreaker(name="combo")` and Spring's `@Retryable(maxRetries=3, delay=10)`
|
|
([`r4j-combo.txt`](output/r4j-combo.txt), [`proxy-payments.txt`](output/proxy-payments.txt)):
|
|
|
|
```
|
|
"call 1": {
|
|
"caller got": "TransientException: payment gateway 503 (call 4)",
|
|
"method body ran": 4,
|
|
"elapsed (ms)": 35,
|
|
"breaker: buffered / failed / not permitted": "4 / 4 / 0",
|
|
"breaker state": "CLOSED"
|
|
},
|
|
"call 2": {
|
|
"caller got": "CallNotPermittedException: CircuitBreaker 'combo' is OPEN and does not permit further calls",
|
|
"method body ran": 1,
|
|
"elapsed (ms)": 33,
|
|
"breaker: buffered / failed / not permitted": "5 / 5 / 3",
|
|
"breaker state": "OPEN"
|
|
},
|
|
```
|
|
|
|
Spring's retry interceptor is the first advisor on the proxy - ahead of every Resilience4j aspect
|
|
(orders 2147483642-2147483646) - so it retries *around* the breaker. The breaker counts each attempt
|
|
as a separate call, and once it opens, the retry spends its remaining attempts on
|
|
`CallNotPermittedException`. With the default 1 s delay that is three seconds of waiting to be told
|
|
the circuit is open.
|
|
|
|
If you combine them, add `excludes = CallNotPermittedException.class` to the `@Retryable` - or use
|
|
Resilience4j's `@Retry`, whose aspect order puts it outside the breaker deliberately and which you
|
|
can configure to ignore `CallNotPermittedException`.
|
|
|
|
## TimeLimiter
|
|
|
|
[`r4j-timelimiter.txt`](output/r4j-timelimiter.txt): a 2 s `CompletableFuture` under a 500 ms
|
|
limit fails with `TimeoutException` after 512 ms. It only applies to asynchronous return types -
|
|
there is no equivalent for a blocking method in either library.
|