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
2.5 KiB
5. Retry and transactions
← 4. @ConcurrencyLimit · Index · Next: 6. What is left for Resilience4j →
StockWriter.record 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).
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 - 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 is
@Transactional and calls the same method (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
covers the rollback-only mechanics.