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.
3.4 KiB
2. The circuit breaker, verified
Previous: 01-two-resilience-stacks.md | README | Next: 03-spring-retryable.md
Source: R4jPaymentService.java.
Config: application.yml.
Test: CircuitBreakerTripAndRecoverTest.java.
Transcript: docs/output/01-circuitbreaker-trip.txt.
The state machine
CLOSED --(failure rate >= threshold, over >= minimumNumberOfCalls)--> OPEN
OPEN --(waitDurationInOpenState elapses)--> HALF_OPEN
HALF_OPEN --(permittedNumberOfCallsInHalfOpenState calls all succeed)--> CLOSED
HALF_OPEN --(a probe call fails)--> OPEN
This repo's config: slidingWindowSize=10, minimumNumberOfCalls=5, failureRateThreshold=50,
waitDurationInOpenState=2s, permittedNumberOfCallsInHalfOpenState=2.
What the transcript actually shows
Calls 1-5 against a dead downstream: each one really calls the downstream, gets a
DownstreamUnavailableException, and the fallback method runs. By call 5 the breaker has seen
5 calls (the configured minimum) at a 100% failure rate and flips to OPEN. Call 6 is the
first call that does not touch the downstream at all — it gets CallNotPermittedException
instead, straight from the breaker, before R4jPaymentService.pay()'s body ever runs.
Calls 7-9 (phase 2 in the transcript) confirm the downstream call counter is frozen at 5 for
the rest of the OPEN period — three more calls, zero more downstream traffic. This is the
property @Retryable cannot offer on its own (see chapter 3): a
circuit breaker remembers that the dependency is down and stops asking.
After waitDurationInOpenState (2s; the test waits 2.2s to be safely past it) the breaker
allows exactly permittedNumberOfCallsInHalfOpenState probe calls through. The downstream was
reconfigured to succeed by then, both probes pass, and the breaker closes.
minimumNumberOfCalls and why it exists
A fresh breaker with one failed call and no minimumNumberOfCalls floor would trip on pure
noise — one deployment-time connection refused, one cold JVM. minimumNumberOfCalls=5 here
means the first four failures cannot trip anything by themselves; the breaker needs a real
sample before it judges the dependency.
Actuator health vs the circuitbreakers endpoint
With management.endpoint.health.show-details: always, GET /actuator/health reports only an
aggregate "circuitBreakers":{"status":"UNKNOWN"} in this version — no per-instance detail.
The full breakdown (state, failureRate, bufferedCalls, notPermittedCalls, ...) is at
GET /actuator/circuitbreakers instead. Both captured live, breaker actually OPEN, via
ActuatorHealthTest.java:
05-actuator-health-tripped.txt.
- Resilience4j reference: CircuitBreaker (
rel=nofollow) - The actual states and transition rules, from the CircuitBreaker interface itself, are worth
reading directly rather than from a diagram:
io.github.resilience4j.circuitbreaker.CircuitBreaker.State.
Next: 03-spring-retryable.md.