Add resilience4j-circuit-breaker: Resilience4j 2.4.0 vs Spring Framework 7 core, on Boot 4.1
Companion module for the rewritten post 'Resilience4j Circuit Breaker in Spring Boot 4.1: What It's Still For', reworked around Framework 7 now shipping @Retryable/@ConcurrencyLimit in core. Covers what's still Resilience4j's job (circuit breaker, rate limiter, bulkhead's bounded wait, fallback methods, Actuator/Micrometer metrics), the off-by-one between maxAttempts and maxRetries, and two Boot-4.1 build breaks: spring-boot-starter-aop no longer exists (renamed to spring-boot-starter-aspectj, proven with Maven Central metadata and the renamed starter's own POM -- see resilience/docs/08-starter-aop-renamed-to-starter-aspectj.md in this same repo), and the resulting fix uses that renamed starter directly rather than assembling spring-aop + aspectjweaver by hand. Kept as its own module rather than a new top-level repository, alongside the existing resilience/ module for the sibling Framework-7 post.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
|
||||
[ERROR] 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-aop:jar is missing. @ line 19, column 17
|
||||
@
|
||||
[ERROR] The build could not read 1 project -> [Help 1]
|
||||
[ERROR]
|
||||
[ERROR] The project com.ankurm:break-demo:1.0.0 (/home/claude/work/repos/resilience-boot4-demo/broken-example/pom.xml) has 1 error
|
||||
[ERROR] 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-aop:jar is missing. @ line 19, column 17
|
||||
[ERROR]
|
||||
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
|
||||
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
|
||||
[ERROR]
|
||||
[ERROR] For more information about the errors and possible solutions, please read the following articles:
|
||||
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException
|
||||
@@ -0,0 +1,24 @@
|
||||
Resilience4j circuit breaker: trip, stay open, half-open, recover
|
||||
=================================================================
|
||||
Config: slidingWindowSize=10, minimumNumberOfCalls=5, failureRateThreshold=50%, waitDurationInOpenState=2s
|
||||
|
||||
-- Phase 1: 6 calls against a dead downstream (only 6, to satisfy minimumNumberOfCalls=5) --
|
||||
call 1 -> FALLBACK for order-1: DownstreamUnavailableException - payment-gateway rejected call #1 [breaker state=CLOSED]
|
||||
call 2 -> FALLBACK for order-2: DownstreamUnavailableException - payment-gateway rejected call #2 [breaker state=CLOSED]
|
||||
call 3 -> FALLBACK for order-3: DownstreamUnavailableException - payment-gateway rejected call #3 [breaker state=CLOSED]
|
||||
call 4 -> FALLBACK for order-4: DownstreamUnavailableException - payment-gateway rejected call #4 [breaker state=CLOSED]
|
||||
call 5 -> FALLBACK for order-5: DownstreamUnavailableException - payment-gateway rejected call #5 [breaker state=OPEN]
|
||||
call 6 -> FALLBACK for order-6: CallNotPermittedException - CircuitBreaker 'paymentService' is OPEN and does not permit further calls [breaker state=OPEN]
|
||||
|
||||
Breaker state after 6 failing calls: OPEN (downstream was actually called 5 times)
|
||||
|
||||
-- Phase 2: 3 more calls while OPEN — these must NOT reach the downstream --
|
||||
call 7 -> FALLBACK for order-7: CallNotPermittedException - CircuitBreaker 'paymentService' is OPEN and does not permit further calls [breaker state=OPEN, downstream calls so far=5]
|
||||
call 8 -> FALLBACK for order-8: CallNotPermittedException - CircuitBreaker 'paymentService' is OPEN and does not permit further calls [breaker state=OPEN, downstream calls so far=5]
|
||||
call 9 -> FALLBACK for order-9: CallNotPermittedException - CircuitBreaker 'paymentService' is OPEN and does not permit further calls [breaker state=OPEN, downstream calls so far=5]
|
||||
Downstream call count unchanged (5) -- the breaker short-circuited all 3 calls without touching the downstream.
|
||||
|
||||
-- Phase 3: wait 2.2s for waitDurationInOpenState, downstream now recovers, probe with permittedNumberOfCallsInHalfOpenState=2 --
|
||||
half-open probe 1 -> OK (call #1) [breaker state=HALF_OPEN]
|
||||
half-open probe 2 -> OK (call #2) [breaker state=CLOSED]
|
||||
Breaker state after 2 successful half-open probes: CLOSED
|
||||
@@ -0,0 +1,6 @@
|
||||
@Retryable(maxRetries=3, delay=200ms, multiplier=2.0): recovering from 2 transient failures
|
||||
===========================================================================================
|
||||
downstream configured to fail its first 2 calls, then succeed
|
||||
result: OK (call #3)
|
||||
downstream was actually called 3 times
|
||||
elapsed: ~601ms (expect >= 200ms delay before the 2nd attempt, plus backoff before the 3rd)
|
||||
@@ -0,0 +1,12 @@
|
||||
@Retryable against a permanently-dead downstream: two consecutive calls, no shared state
|
||||
========================================================================================
|
||||
-- Call 1: pay("order-A") --
|
||||
threw FlakyDownstream.DownstreamUnavailableException after exhausting retries
|
||||
downstream calls so far: 4 (1 initial attempt + 3 retries = 4 expected)
|
||||
|
||||
-- Call 2: pay("order-B"), immediately after Call 1 exhausted its retries --
|
||||
downstream calls so far: 8 (another 4 attempts, not fast-failed)
|
||||
|
||||
Contrast with docs/output/01-circuitbreaker-trip.txt: there, calls 7-9 after the trip
|
||||
added ZERO downstream calls. Here, call 2 pays the same 4-attempt cost as call 1.
|
||||
@Retryable has no OPEN state -- it cannot tell you 'this dependency is currently down'.
|
||||
@@ -0,0 +1,4 @@
|
||||
@Retryable via self-invocation: the AOP proxy trap, same one that bites Resilience4j
|
||||
====================================================================================
|
||||
Calling payViaSelfInvocation(...), which calls this.pay(...) from inside the same bean.
|
||||
downstream calls: 1 (expected 1 -- no retry happened; the proxy was bypassed)
|
||||
@@ -0,0 +1,6 @@
|
||||
Resilience4j @Retry(maxAttempts=3, waitDuration=200ms, exponentialBackoffMultiplier=2): recovering from 1 transient failure
|
||||
===========================================================================================================================
|
||||
downstream configured to fail its first call, then succeed
|
||||
result: OK (call #2)
|
||||
downstream was actually called 2 times
|
||||
elapsed: ~206ms (expect >= 200ms wait before the 2nd attempt)
|
||||
@@ -0,0 +1,8 @@
|
||||
Resilience4j @Retry(maxAttempts=3) against a permanently-dead downstream: counting convention
|
||||
=============================================================================================
|
||||
maxAttempts=3, downstream permanently down
|
||||
downstream calls before giving up: 3
|
||||
Resilience4j's maxAttempts is the TOTAL call count (initial attempt included): 3, not 4.
|
||||
Core's @Retryable(maxRetries=3) is 3 retries AFTER the initial attempt: 4 total
|
||||
(see docs/output/03b-retryable-no-memory.txt). Same-sounding config, different arithmetic --
|
||||
porting a maxRetries value from one to the other by name alone is off by one.
|
||||
@@ -0,0 +1,7 @@
|
||||
@ConcurrencyLimit(limit=2, policy=BLOCK), 4 concurrent callers, each sleeps 300ms
|
||||
=================================================================================
|
||||
per-caller completion time (ms), sorted: [300, 300, 598, 600]
|
||||
total wall time for all 4 callers: 601ms
|
||||
all 4 calls succeeded (BLOCK never rejects): true
|
||||
expectation: with limit=2 and 300ms per call, 4 callers must take roughly 2x300=600ms+,
|
||||
not ~300ms as they would with no limit at all.
|
||||
@@ -0,0 +1,10 @@
|
||||
@ConcurrencyLimit(limit=2, policy=REJECT), 4 concurrent callers, each sleeps 300ms
|
||||
==================================================================================
|
||||
OK in 301ms
|
||||
REJECTED (InvocationRejectedException) in 0ms
|
||||
OK in 300ms
|
||||
REJECTED (InvocationRejectedException) in 0ms
|
||||
|
||||
OK: 2, REJECTED: 2 (expected 2 and 2 with limit=2, 4 callers)
|
||||
Rejection throws org.springframework.resilience.InvocationRejectedException
|
||||
(a RejectedExecutionException subtype) -- confirmed by javap, not documented on the annotation itself.
|
||||
@@ -0,0 +1,9 @@
|
||||
Resilience4j @Bulkhead(maxConcurrentCalls=2, maxWaitDuration=100ms), same 4-caller/300ms shape
|
||||
==============================================================================================
|
||||
REJECTED:c0 in 112ms
|
||||
done:c1 in 307ms
|
||||
done:c2 in 303ms
|
||||
REJECTED:c3 in 101ms
|
||||
|
||||
REJECTED count: 2 -- these callers waited up to maxWaitDuration=100ms for a slot,
|
||||
then gave up and ran the fallback method, instead of blocking indefinitely like @ConcurrencyLimit's BLOCK policy.
|
||||
@@ -0,0 +1,7 @@
|
||||
Real /actuator/health and /actuator/circuitbreakers, captured over HTTP with the breaker actually OPEN
|
||||
======================================================================================================
|
||||
-- GET /actuator/health --
|
||||
{"components":{"circuitBreakers":{"status":"UNKNOWN"},"diskSpace":{"details":{"total":270553174016,"free":31630450688,"threshold":10485760,"path":"/home/claude/work/repos/spring-boot-demo-clone/resilience4j-circuit-breaker/.","exists":true},"status":"UP"},"livenessState":{"status":"UP"},"ping":{"status":"UP"},"readinessState":{"status":"UP"},"ssl":{"details":{"expiringChains":[],"invalidChains":[],"validChains":[]},"status":"UP"}},"groups":["liveness","readiness"],"status":"UP"}
|
||||
|
||||
-- GET /actuator/circuitbreakers --
|
||||
{"circuitBreakers":{"paymentService":{"bufferedCalls":5,"failedCalls":5,"failureRate":"100.0%","failureRateThreshold":"50.0%","notPermittedCalls":1,"slowCallRate":"0.0%","slowCallRateThreshold":"100.0%","slowCalls":0,"slowFailedCalls":0,"state":"OPEN"}}}
|
||||
Reference in New Issue
Block a user