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
52 lines
2.5 KiB
Markdown
52 lines
2.5 KiB
Markdown
# 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.
|