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
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# Spring Framework 7's built-in resilience - and what is left for Resilience4j
|
||||
|
||||
Companion project for [**Spring Framework 7's Built-in Resilience: @Retryable, @ConcurrencyLimit, and What's Left for Resilience4j**](https://ankurm.com/spring-framework-7-retryable-concurrencylimit-resilience4j/)
|
||||
on ankurm.com.
|
||||
|
||||
Every behaviour the article states was counted here - by recording each real invocation of the
|
||||
guarded method, not by trusting the retry machinery's own view. `./scripts/run-all.sh`
|
||||
regenerates every transcript in [`docs/output/`](docs/output).
|
||||
|
||||
## Versions
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Spring Boot | 4.1.1 |
|
||||
| Spring Framework | 7.0.9 (`org.springframework.resilience`, `org.springframework.core.retry`) |
|
||||
| Resilience4j | 2.4.0 (`resilience4j-spring-boot4`, not managed by Boot - pin it) |
|
||||
| JDK | Eclipse Temurin 25.0.4.1 (LTS) |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/path/to/jdk-25
|
||||
mvn -DskipTests package
|
||||
./scripts/run.sh # port 8081
|
||||
curl -s localhost:8081/demo/retry/exhausted | python3 -m json.tool
|
||||
./scripts/run-all.sh # about 90 s: some scenarios sleep through 1 s delays
|
||||
mvn test # 16 contract tests
|
||||
```
|
||||
|
||||
## Scenarios
|
||||
|
||||
| Endpoint | Shows |
|
||||
|---|---|
|
||||
| `/demo/retry/{defaults,exhausted,exponential,jitter,includes,cause,future,mono,timeout,hang,self-invocation}` | what `@Retryable` does, counted |
|
||||
| `/demo/tx/{standalone,joined}` | `@Retryable` + `@Transactional`, alone and inside a caller's transaction |
|
||||
| `/demo/limit/{none,block,reject,limit-and-retry,r4j-bulkhead}` | `@ConcurrencyLimit` policies, and Resilience4j's bulkhead for comparison |
|
||||
| `/demo/r4j/{retry,breaker,combo,timelimiter}` | what only Resilience4j does, and both libraries on one method |
|
||||
| `/demo/proxy/{payments,stock-writer,reports}` | the advisor chain on each proxy, outermost first |
|
||||
|
||||
All of these are diagnostics for the article. Delete [`DemoController`](src/main/java/com/ankurm/resilience/web/DemoController.java) before shipping anything.
|
||||
|
||||
## Documentation
|
||||
|
||||
1. [What Spring Framework 7 ships, and in which 7.0.x release](docs/01-whats-in-framework-7.md)
|
||||
2. [Switching it on - and the three ways it stays off](docs/02-enabling.md)
|
||||
3. [`@Retryable`, measured](docs/03-retryable-measured.md)
|
||||
4. [`@ConcurrencyLimit`: BLOCK, REJECT, and nesting with retry](docs/04-concurrency-limit.md)
|
||||
5. [Retry and transactions](docs/05-retry-and-transactions.md)
|
||||
6. [What is left for Resilience4j](docs/06-what-is-left-for-resilience4j.md)
|
||||
7. [Migrating from spring-retry](docs/07-migrating-from-spring-retry.md)
|
||||
|
||||
## Findings worth the trip
|
||||
|
||||
- **`maxRetries = 3` is four invocations.** Resilience4j's `maxAttempts: 3` is three, and so was
|
||||
spring-retry's `maxAttempts`. A mechanical migration adds one call.
|
||||
- **`timeout` is a budget checked between attempts, not a call timeout.** A 1.5 s attempt under a
|
||||
500 ms timeout runs its full 1.5 s.
|
||||
- **Jitter only ever adds delay** (for the first delay, and for every delay when `multiplier` is 1):
|
||||
`delay=200, jitter=100` produced gaps of 211-296 ms, never below 200.
|
||||
- **A `CompletableFuture` that completes exceptionally is not retried**; a `Mono` is.
|
||||
- **`@Retryable` is placed outside `@Transactional` and outside Resilience4j** regardless of any
|
||||
`order` - both post-processors call `setBeforeExistingAdvisors(true)`. Good for transactions, bad
|
||||
for a circuit breaker: an open circuit is retried three times.
|
||||
- **`timeout` and `ConcurrencyLimit.ThrottlePolicy.REJECT` did not exist in 7.0.0.** `timeout`
|
||||
arrived in 7.0.2, `policy` in 7.0.3.
|
||||
Reference in New Issue
Block a user