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:
2026-09-11 17:12:25 +00:00
co-authored by Claude Opus 5
parent 926250e1a9
commit 7e1676c763
61 changed files with 2155 additions and 0 deletions
@@ -0,0 +1,45 @@
# 1. What Spring Framework 7 ships
[Index](../README.md) · Next: [2. Enabling →](02-enabling.md)
| Piece | Package | Jar |
|---|---|---|
| `@Retryable`, `@ConcurrencyLimit`, `@EnableResilientMethods` | `org.springframework.resilience.annotation` | spring-context |
| `MethodRetryEvent`, `MethodRetryPredicate`, interceptors | `org.springframework.resilience.retry` | spring-context |
| `InvocationRejectedException` | `org.springframework.resilience` | spring-context |
| `RetryTemplate`, `RetryPolicy`, `RetryListener`, `RetryException` | `org.springframework.core.retry` | spring-core |
## Defaults, read from the annotations
Reflection over `Retryable.class.getDeclaredMethods()` on 7.0.9
([`ApiSurfaceTest`](../src/test/java/com/ankurm/resilience/ApiSurfaceTest.java)):
| Attribute | Default |
|---|---|
| `maxRetries` | `3` - retries *after* the first call |
| `delay` | `1000` (ms) |
| `multiplier` | `1.0` - no growth |
| `maxDelay` | `Long.MAX_VALUE` |
| `jitter` | `0` |
| `timeout` | `0` - no budget |
| `timeUnit` | `MILLISECONDS` |
| `includes` / `excludes` | empty - every exception is retryable |
`@ConcurrencyLimit` has no usable default limit (`Integer.MIN_VALUE` means "not set") and
`policy = BLOCK`. `@EnableResilientMethods` has `proxyTargetClass = false` and
`order = Integer.MAX_VALUE - 1`.
## It moved during 7.0.x
[`api-surface.txt`](output/api-surface.txt), per release:
| Added in | What |
|---|---|
| 7.0.0 | `@Retryable` (no `timeout`), `@ConcurrencyLimit` (no `policy`), `RetryTemplate`, `RetryPolicy` |
| 7.0.2 | `@Retryable.timeout`/`timeoutString`, `RetryPolicy.Builder.timeout(..)`, `RetryListener.onRetryPolicyTimeout` and `onRetryableExecution` |
| 7.0.3 | `@ConcurrencyLimit.policy` with `ThrottlePolicy.BLOCK` / `REJECT` |
Release dates from the Maven Central `Last-Modified` headers: 7.0.0 on 13 Nov 2025, 7.0.2 on
11 Dec 2025, 7.0.3 on 15 Jan 2026. Articles written against the 7.0 GA - including the Spring reference page at the time
this was written, which does not mention `timeout` or `policy` - describe a smaller API than the
one you have.
+33
View File
@@ -0,0 +1,33 @@
# 2. Switching it on - and the three ways it stays off
[← 1. What ships](01-whats-in-framework-7.md) · [Index](../README.md) · Next: [3. @Retryable measured →](03-retryable-measured.md)
## Spring Boot 4.1.1 does not enable it
No auto-configuration in Boot 4.1.1 registers `RetryAnnotationBeanPostProcessor` or
`ConcurrencyLimitBeanPostProcessor` (no Boot 4.1.1 jar in this project's dependency tree mentions
either class). You need [`@EnableResilientMethods`](../src/main/java/com/ankurm/resilience/ResilienceConfig.java).
Without it the annotations are metadata. [`retry-disabled.txt`](output/retry-disabled.txt), same
method, `demo.resilience.enabled=false`:
```
"invocations": 1,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 1 failed"
```
No warning at startup, no log line at call time.
## Self-invocation
A call from inside the bean does not go through the proxy
([`retry-self-invocation.txt`](output/retry-self-invocation.txt)): one invocation, first exception
straight to the caller. Same rule as `@Transactional` and `@Async` - see the
[AOP article](https://ankurm.com/spring-aop-pointcuts-advice-types-aspect-not-firing/).
## `final` methods
The proxies here are CGLIB subclasses (`/demo/proxy/*` reports the type - the demo beans implement
no interface). A subclass cannot override a `final` method, so a `final @Retryable` method is
called directly on the target and never retried - the same silent failure measured for `@Async` in
the [@Async article](https://ankurm.com/spring-boot-4-async-executors-virtual-threads/).
+61
View File
@@ -0,0 +1,61 @@
# 3. `@Retryable`, measured
[← 2. Enabling](02-enabling.md) · [Index](../README.md) · Next: [4. @ConcurrencyLimit →](04-concurrency-limit.md)
Every number here is from [`FlakyGateway`](../src/main/java/com/ankurm/resilience/retry/FlakyGateway.java),
which records each real invocation in [`CallLog`](../src/main/java/com/ankurm/resilience/support/CallLog.java).
## Counting
| Scenario | Invocations | Gaps (ms) | Caller got | Transcript |
|---|---|---|---|---|
| defaults, fails twice | 3 | 1009, 1001 | the value | [`retry-defaults.txt`](output/retry-defaults.txt) |
| defaults, always fails | **4** | 1001, 1001, 1002 | `TransientException: attempt 4 failed` | [`retry-exhausted.txt`](output/retry-exhausted.txt) |
| `maxRetries=5, delay=100, multiplier=2, maxDelay=500` | 6 | 101, 200, 401, 501, 501 | last exception | [`retry-exponential.txt`](output/retry-exponential.txt) |
| `delay=200, jitter=100` | 7 | 254, 211, 296, 294, 273, 293 | last exception | [`retry-jitter.txt`](output/retry-jitter.txt) |
- The caller receives the **last original exception**, not a wrapper. (`RetryTemplate.execute`
throws `RetryException`; the annotation path unwraps it.)
- `maxDelay` caps the exponential sequence: 400 would have been 800.
## Jitter only adds
Jitter looks symmetric in the documentation. `ExponentialBackOff$ExponentialBackOffExecution.applyJitter`
(read with `javap -c` on spring-core 7.0.9) computes the range as
`[max(interval - j, initialInterval), min(interval + j, maxInterval)]`, where `j` is the jitter
scaled by `interval / initialInterval`. For the first delay - and for every delay when `multiplier`
is 1 - the lower bound is clamped to `delay` itself, so jitter can only lengthen the wait. The
transcript agrees: six gaps between 211 and 296 ms, none under 200.
## What is retried
| Scenario | Invocations | Why |
|---|---|---|
| `includes = IllegalStateException`, throws `IllegalArgumentException` | 1 | not included |
| `includes = IOException`, throws `UncheckedIOException(IOException)` | 3 | **causes are matched** |
| returns `CompletableFuture.failedFuture(...)` | **1** | the method returned normally |
| returns `Mono.fromCallable(...)` that errors | 3 | retried by re-subscription |
The `CompletableFuture` row is the one to remember: an async client method that returns a future
is not protected by `@Retryable` at all ([`retry-future.txt`](output/retry-future.txt)). Reactive
types go through a Reactor `retryWhen`, so a `Mono` works.
## `timeout` is a budget, not a timeout
| Scenario | Invocations | Elapsed | |
|---|---|---|---|
| attempts of 300 ms, `delay=100`, `timeout=1000` | 3 | 1105 ms | stops once the budget is spent - after overrunning it by one attempt |
| one attempt of 1500 ms, `timeout=500` | 1 | **1502 ms** | the attempt is not interrupted |
The budget is checked between attempts. Use a client-level timeout (HTTP client, JDBC query
timeout) for the call itself, and treat `timeout` as "stop retrying after roughly this long".
## Events are not a "retry happened" signal
`MethodRetryEvent` fires for every failure with `isRetryAborted() == false`, *including the last
one*, then once more with a `RetryException` and `isRetryAborted() == true`. For an exception that
is not even retryable you get `will retry` followed by `retry aborted`
([`retry-includes.txt`](output/retry-includes.txt)). A counter of events with `retryAborted=false`
counts failures, not retries. Spring registers no retry metrics of its own
([`metrics-names.txt`](output/metrics-names.txt)): `app.retry.failures` exists only because
[`RetryEvents`](../src/main/java/com/ankurm/resilience/retry/RetryEvents.java) creates it.
+33
View File
@@ -0,0 +1,33 @@
# 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.
@@ -0,0 +1,51 @@
# 5. Retry and transactions
[← 4. @ConcurrencyLimit](04-concurrency-limit.md) · [Index](../README.md) · Next: [6. What is left for Resilience4j →](06-what-is-left-for-resilience4j.md)
[`StockWriter.record`](../src/main/java/com/ankurm/resilience/tx/StockWriter.java) is
`@Retryable @Transactional`, inserts a row, and fails twice.
## Which interceptor is outside
`RetryAnnotationBeanPostProcessor`'s constructor calls `setBeforeExistingAdvisors(true)` (`javap -c`
on spring-context 7.0.9). The transaction advisor is applied first by the auto-proxy creator; the
retry advisor is then inserted at index 0 ([`proxy-stock-writer.txt`](output/proxy-stock-writer.txt)).
The `order` attribute of `@EnableResilientMethods` is the post-processor's order, not the
advice's position, and does not change this.
## Alone: each attempt is its own transaction
[`tx-standalone.txt`](output/tx-standalone.txt) - three connection holders, one row:
```
"attempts": [
"attempt 1: transaction active=true, connection holder @6aa73704",
"attempt 2: transaction active=true, connection holder @1978b580",
"attempt 3: transaction active=true, connection holder @7d5534c2"
],
"rows in stock_movement": 1
```
This is what you want: the failed attempts rolled back their inserts.
## Called from inside a transaction: the retry cannot help
[`OrderFacade.placeOrder`](../src/main/java/com/ankurm/resilience/tx/OrderFacade.java) is
`@Transactional` and calls the same method ([`tx-joined.txt`](output/tx-joined.txt)):
```
"caller got": "org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only",
"attempts": [
"attempt 1: transaction active=true, connection holder @6135f416",
"attempt 2: transaction active=true, connection holder @6135f416",
"attempt 3: transaction active=true, connection holder @6135f416"
],
"rows in stock_movement": 0
```
Every attempt joins the outer transaction (`REQUIRED`). The first failure passes through the inner
`TransactionInterceptor`, which marks the *shared* transaction rollback-only. The third attempt
succeeds, the retry returns normally, and the outer commit throws. Three attempts, zero rows, an
exception the caller did not expect. The retry belongs at the outermost transactional boundary -
or the inner method needs `REQUIRES_NEW`. The [@Transactional article](https://ankurm.com/transactional-propagation-isolation-silent-failures/)
covers the rollback-only mechanics.
@@ -0,0 +1,68 @@
# 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.
@@ -0,0 +1,21 @@
# 7. Migrating from spring-retry
[← 6. What is left for Resilience4j](06-what-is-left-for-resilience4j.md) · [Index](../README.md)
The spring-retry README now says the project "is no longer maintained as an open-source project"
and "has been superseded by Spring Framework 7". Its last release on Maven Central is 2.0.13.
| spring-retry | Spring Framework 7 | Watch out |
|---|---|---|
| `@EnableRetry` | `@EnableResilientMethods` | |
| `@Retryable(maxAttempts = 3)` | `@Retryable(maxRetries = 2)` | **attempts vs retries** - `maxRetries = 3` is one call more |
| `@Retryable(retryFor = X.class)` | `@Retryable(includes = X.class)` (or `value`) | causes are matched too |
| `@Retryable(noRetryFor = X.class)` | `@Retryable(excludes = X.class)` | |
| `@Backoff(delay, multiplier, maxDelay)` | `delay`, `multiplier`, `maxDelay` attributes | default delay is 1000 ms in both |
| `@Backoff(random = true)` | `jitter` | jitter only adds delay (chapter 3) |
| `@Recover` | none - catch the exception in the caller | declined upstream |
| `RetryTemplate.builder()` | `new RetryTemplate(RetryPolicy.builder()...build())` | `execute` throws `RetryException`; `invoke` throws the original |
| stateful retry / `RetryContext` | none | |
Keep spring-retry on the classpath only as long as something still uses `@Recover` or stateful
retry. Both libraries define an annotation named `Retryable` - check the import on every one.
+31
View File
@@ -0,0 +1,31 @@
# Public API of the resilience support, per Spring Framework release
## 7.0.0
@Retryable [delay, delayString, excludes, includes, jitter, jitterString, maxDelay, maxDelayString, maxRetries, maxRetriesString, multiplier, multiplierString, predicate, timeUnit, value]
@ConcurrencyLimit [limit, limitString, value]
RetryPolicy.Builder [backOff, build, delay, excludes, includes, jitter, maxDelay, maxRetries, multiplier, predicate]
RetryListener [beforeRetry, onRetryFailure, onRetryPolicyExhaustion, onRetryPolicyInterruption, onRetrySuccess]
## 7.0.1
@Retryable [delay, delayString, excludes, includes, jitter, jitterString, maxDelay, maxDelayString, maxRetries, maxRetriesString, multiplier, multiplierString, predicate, timeUnit, value]
@ConcurrencyLimit [limit, limitString, value]
RetryPolicy.Builder [backOff, build, delay, excludes, includes, jitter, maxDelay, maxRetries, multiplier, predicate]
RetryListener [beforeRetry, onRetryFailure, onRetryPolicyExhaustion, onRetryPolicyInterruption, onRetrySuccess]
## 7.0.2
@Retryable [delay, delayString, excludes, includes, jitter, jitterString, maxDelay, maxDelayString, maxRetries, maxRetriesString, multiplier, multiplierString, predicate, timeUnit, timeout, timeoutString, value]
@ConcurrencyLimit [limit, limitString, value]
RetryPolicy.Builder [backOff, build, delay, excludes, includes, jitter, maxDelay, maxRetries, multiplier, predicate, timeout]
RetryListener [beforeRetry, onRetryFailure, onRetryPolicyExhaustion, onRetryPolicyInterruption, onRetryPolicyTimeout, onRetrySuccess, onRetryableExecution]
## 7.0.3
@Retryable [delay, delayString, excludes, includes, jitter, jitterString, maxDelay, maxDelayString, maxRetries, maxRetriesString, multiplier, multiplierString, predicate, timeUnit, timeout, timeoutString, value]
@ConcurrencyLimit [limit, limitString, policy, value]
RetryPolicy.Builder [backOff, build, delay, excludes, includes, jitter, maxDelay, maxRetries, multiplier, predicate, timeout]
RetryListener [beforeRetry, onRetryFailure, onRetryPolicyExhaustion, onRetryPolicyInterruption, onRetryPolicyTimeout, onRetrySuccess, onRetryableExecution]
## 7.0.9
@Retryable [delay, delayString, excludes, includes, jitter, jitterString, maxDelay, maxDelayString, maxRetries, maxRetriesString, multiplier, multiplierString, predicate, timeUnit, timeout, timeoutString, value]
@ConcurrencyLimit [limit, limitString, policy, value]
RetryPolicy.Builder [backOff, build, delay, excludes, includes, jitter, maxDelay, maxRetries, multiplier, predicate, timeout]
RetryListener [beforeRetry, onRetryFailure, onRetryPolicyExhaustion, onRetryPolicyInterruption, onRetryPolicyTimeout, onRetrySuccess, onRetryableExecution]
+10
View File
@@ -0,0 +1,10 @@
# GET /demo/limit/block (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"callers": 10,
"succeeded": 10,
"failed": {},
"max concurrently inside the method": 2,
"elapsed (ms)": 1006
}
@@ -0,0 +1,10 @@
# GET /demo/limit/limit-and-retry (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"callers": 2,
"succeeded": 2,
"failed": {},
"max concurrently inside the method": 1,
"elapsed (ms)": 815
}
+10
View File
@@ -0,0 +1,10 @@
# GET /demo/limit/none (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"callers": 10,
"succeeded": 10,
"failed": {},
"max concurrently inside the method": 10,
"elapsed (ms)": 203
}
@@ -0,0 +1,12 @@
# GET /demo/limit/r4j-bulkhead (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"callers": 10,
"succeeded": 2,
"failed": {
"io.github.resilience4j.bulkhead.BulkheadFullException": 8
},
"max concurrently inside the method": 2,
"elapsed (ms)": 231
}
+12
View File
@@ -0,0 +1,12 @@
# GET /demo/limit/reject (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"callers": 10,
"succeeded": 2,
"failed": {
"org.springframework.resilience.InvocationRejectedException": 8
},
"max concurrently inside the method": 2,
"elapsed (ms)": 205
}
+14
View File
@@ -0,0 +1,14 @@
# Spring retry has no metrics of its own; the only meter is the one RetryEvents registers.
$ curl /actuator/metrics | grep -iE 'retry|resilience4j'
app.retry.failures
resilience4j.bulkhead.available.concurrent.calls
resilience4j.bulkhead.max.allowed.concurrent.calls
resilience4j.circuitbreaker.buffered.calls
resilience4j.circuitbreaker.calls
resilience4j.circuitbreaker.failure.rate
resilience4j.circuitbreaker.not.permitted.calls
resilience4j.circuitbreaker.slow.call.rate
resilience4j.circuitbreaker.slow.calls
resilience4j.circuitbreaker.state
resilience4j.retry.calls
resilience4j.timelimiter.calls
+7
View File
@@ -0,0 +1,7 @@
# GET /demo/proxy/gateway (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
[
"proxy type: CGLIB",
"org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor$RetryAnnotationInterceptor order=2147483647"
]
+12
View File
@@ -0,0 +1,12 @@
# GET /demo/proxy/payments (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
[
"proxy type: CGLIB",
"org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor$RetryAnnotationInterceptor order=2147483647",
"org.springframework.aop.interceptor.ExposeInvocationInterceptor order=-2147483647",
"org.springframework.aop.aspectj.AspectJAroundAdvice order=2147483642",
"org.springframework.aop.aspectj.AspectJAroundAdvice order=2147483643",
"org.springframework.aop.aspectj.AspectJAroundAdvice order=2147483645",
"org.springframework.aop.aspectj.AspectJAroundAdvice order=2147483646"
]
+8
View File
@@ -0,0 +1,8 @@
# GET /demo/proxy/reports (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
[
"proxy type: CGLIB",
"org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcessor$ConcurrencyLimitInterceptor order=2147483647",
"org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor$RetryAnnotationInterceptor order=2147483647"
]
@@ -0,0 +1,8 @@
# GET /demo/proxy/stock-writer (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
[
"proxy type: CGLIB",
"org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor$RetryAnnotationInterceptor order=2147483647",
"org.springframework.transaction.interceptor.TransactionInterceptor order=2147483647"
]
+22
View File
@@ -0,0 +1,22 @@
# GET /demo/r4j/breaker (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"calls made": 11,
"calls that reached the downstream": 8,
"of which before the breaker opened": 5,
"outcomes": [
"1: TransientException: payment gateway 503 (call 1) [state after: CLOSED]",
"2: TransientException: payment gateway 503 (call 2) [state after: CLOSED]",
"3: TransientException: payment gateway 503 (call 3) [state after: CLOSED]",
"4: TransientException: payment gateway 503 (call 4) [state after: CLOSED]",
"5: TransientException: payment gateway 503 (call 5) [state after: OPEN]",
"6: CallNotPermittedException: CircuitBreaker 'payments' is OPEN and does not permit further calls [state after: OPEN]",
"7: CallNotPermittedException: CircuitBreaker 'payments' is OPEN and does not permit further calls [state after: OPEN]",
"8: CallNotPermittedException: CircuitBreaker 'payments' is OPEN and does not permit further calls [state after: OPEN]",
"-- 2.1 s later, downstream recovered --",
"9: returned charged [state after: HALF_OPEN]",
"10: returned charged [state after: CLOSED]",
"11: returned charged [state after: CLOSED]"
]
}
+31
View File
@@ -0,0 +1,31 @@
# GET /demo/r4j/combo (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"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"
},
"MethodRetryEvents": [
"chargeWithBoth failed with TransientException -> will retry",
"chargeWithBoth failed with TransientException -> will retry",
"chargeWithBoth failed with TransientException -> will retry",
"chargeWithBoth failed with TransientException -> will retry",
"chargeWithBoth failed with RetryException -> retry aborted",
"chargeWithBoth failed with TransientException -> will retry",
"chargeWithBoth failed with CallNotPermittedException -> will retry",
"chargeWithBoth failed with CallNotPermittedException -> will retry",
"chargeWithBoth failed with CallNotPermittedException -> will retry",
"chargeWithBoth failed with RetryException -> retry aborted"
]
}
+17
View File
@@ -0,0 +1,17 @@
# GET /demo/r4j/retry (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 3,
"started at (ms)": [
7,
26,
36
],
"gaps (ms)": [
19,
10
],
"elapsed (ms)": 37,
"caller": "threw: com.ankurm.resilience.support.TransientException: payment gateway 503 (call 3)"
}
@@ -0,0 +1,12 @@
# GET /demo/r4j/timelimiter (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 1,
"started at (ms)": [
11
],
"gaps (ms)": [],
"elapsed (ms)": 512,
"caller": "threw: java.util.concurrent.TimeoutException: TimeLimiter 'slow' recorded a timeout exception."
}
+23
View File
@@ -0,0 +1,23 @@
# GET /demo/retry/cause (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 3,
"started at (ms)": [
1,
11,
23
],
"gaps (ms)": [
10,
12
],
"elapsed (ms)": 26,
"caller": "threw: java.io.UncheckedIOException: java.io.IOException: socket reset on attempt 3",
"MethodRetryEvents": [
"wrappedCause failed with UncheckedIOException -> will retry",
"wrappedCause failed with UncheckedIOException -> will retry",
"wrappedCause failed with UncheckedIOException -> will retry",
"wrappedCause failed with RetryException -> retry aborted"
]
}
+21
View File
@@ -0,0 +1,21 @@
# GET /demo/retry/defaults (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 3,
"started at (ms)": [
27,
1036,
2037
],
"gaps (ms)": [
1009,
1001
],
"elapsed (ms)": 2037,
"caller": "returned: ok after 3 invocation(s)",
"MethodRetryEvents": [
"defaults failed with TransientException -> will retry",
"defaults failed with TransientException -> will retry"
]
}
+14
View File
@@ -0,0 +1,14 @@
# The same @Retryable method with @EnableResilientMethods switched off (demo.resilience.enabled=false)
{
"invocations": 1,
"started at (ms)": [
0
],
"gaps (ms)": [],
"elapsed (ms)": 0,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 1 failed"
}
# Startup log lines mentioning retry or resilience:
(none)
@@ -0,0 +1,26 @@
# GET /demo/retry/exhausted (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 4,
"started at (ms)": [
0,
1001,
2002,
3004
],
"gaps (ms)": [
1001,
1001,
1002
],
"elapsed (ms)": 3008,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 4 failed",
"MethodRetryEvents": [
"defaults failed with TransientException -> will retry",
"defaults failed with TransientException -> will retry",
"defaults failed with TransientException -> will retry",
"defaults failed with TransientException -> will retry",
"defaults failed with RetryException -> retry aborted"
]
}
@@ -0,0 +1,32 @@
# GET /demo/retry/exponential (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 6,
"started at (ms)": [
3,
104,
304,
705,
1206,
1707
],
"gaps (ms)": [
101,
200,
401,
501,
501
],
"elapsed (ms)": 1707,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 6 failed",
"MethodRetryEvents": [
"exponential failed with TransientException -> will retry",
"exponential failed with TransientException -> will retry",
"exponential failed with TransientException -> will retry",
"exponential failed with TransientException -> will retry",
"exponential failed with TransientException -> will retry",
"exponential failed with TransientException -> will retry",
"exponential failed with RetryException -> retry aborted"
]
}
+12
View File
@@ -0,0 +1,12 @@
# GET /demo/retry/future (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 1,
"started at (ms)": [
0
],
"gaps (ms)": [],
"elapsed (ms)": 1,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 1 failed"
}
+16
View File
@@ -0,0 +1,16 @@
# GET /demo/retry/hang (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 1,
"started at (ms)": [
1
],
"gaps (ms)": [],
"elapsed (ms)": 1502,
"caller": "threw: com.ankurm.resilience.support.TransientException: slow failure",
"MethodRetryEvents": [
"hangingWithTimeout failed with TransientException -> will retry",
"hangingWithTimeout failed with RetryException -> retry aborted"
]
}
+16
View File
@@ -0,0 +1,16 @@
# GET /demo/retry/includes (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 1,
"started at (ms)": [
1
],
"gaps (ms)": [],
"elapsed (ms)": 0,
"caller": "threw: java.lang.IllegalArgumentException: not in includes",
"MethodRetryEvents": [
"onlyIllegalState failed with IllegalArgumentException -> will retry",
"onlyIllegalState failed with RetryException -> retry aborted"
]
}
+35
View File
@@ -0,0 +1,35 @@
# GET /demo/retry/jitter (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 7,
"started at (ms)": [
0,
254,
465,
761,
1055,
1328,
1621
],
"gaps (ms)": [
254,
211,
296,
294,
273,
293
],
"elapsed (ms)": 1622,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 7 failed",
"MethodRetryEvents": [
"jittered failed with TransientException -> will retry",
"jittered failed with TransientException -> will retry",
"jittered failed with TransientException -> will retry",
"jittered failed with TransientException -> will retry",
"jittered failed with TransientException -> will retry",
"jittered failed with TransientException -> will retry",
"jittered failed with TransientException -> will retry",
"jittered failed with RetryException -> retry aborted"
]
}
+21
View File
@@ -0,0 +1,21 @@
# GET /demo/retry/mono (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 3,
"started at (ms)": [
37,
65,
75
],
"gaps (ms)": [
28,
10
],
"elapsed (ms)": 75,
"caller": "returned: ok after 3 invocation(s)",
"MethodRetryEvents": [
"mono failed with TransientException -> will retry",
"mono failed with TransientException -> will retry"
]
}
@@ -0,0 +1,12 @@
# GET /demo/retry/self-invocation (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 1,
"started at (ms)": [
0
],
"gaps (ms)": [],
"elapsed (ms)": 0,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 1 failed"
}
+23
View File
@@ -0,0 +1,23 @@
# GET /demo/retry/timeout (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"invocations": 3,
"started at (ms)": [
303,
705,
1106
],
"gaps (ms)": [
402,
401
],
"elapsed (ms)": 1105,
"caller": "threw: com.ankurm.resilience.support.TransientException: attempt 3 failed",
"MethodRetryEvents": [
"slowWithTimeout failed with TransientException -> will retry",
"slowWithTimeout failed with TransientException -> will retry",
"slowWithTimeout failed with TransientException -> will retry",
"slowWithTimeout failed with RetryException -> retry aborted"
]
}
+12
View File
@@ -0,0 +1,12 @@
# GET /demo/tx/joined (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"caller got": "org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only",
"attempts": [
"attempt 1: transaction active=true, connection holder @6135f416",
"attempt 2: transaction active=true, connection holder @6135f416",
"attempt 3: transaction active=true, connection holder @6135f416"
],
"rows in stock_movement": 0
}
+12
View File
@@ -0,0 +1,12 @@
# GET /demo/tx/standalone (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)
# Source: src/main/java/com/ankurm/resilience/web/DemoController.java
{
"caller got": "success",
"attempts": [
"attempt 1: transaction active=true, connection holder @6aa73704",
"attempt 2: transaction active=true, connection holder @1978b580",
"attempt 3: transaction active=true, connection holder @7d5534c2"
],
"rows in stock_movement": 1
}