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,50 @@
|
||||
package com.ankurm.resilience;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* A downstream call whose failure pattern is controlled by the test, not by chance.
|
||||
* A deterministic fake beats a flaky "randomly fails 30% of the time" for teaching:
|
||||
* the reader can predict exactly which call will fail and check the transcript against it.
|
||||
*
|
||||
* See docs/01-two-resilience-stacks.md.
|
||||
*/
|
||||
@Component
|
||||
public class FlakyDownstream {
|
||||
|
||||
private final AtomicInteger callCount = new AtomicInteger();
|
||||
|
||||
/** Calls 1..failUntilInclusive throw; calls after that succeed. Set failUntilInclusive
|
||||
* to Integer.MAX_VALUE to simulate a downstream that never recovers. */
|
||||
private volatile int failUntilInclusive = 0;
|
||||
|
||||
public void configure(int failUntilInclusive) {
|
||||
this.failUntilInclusive = failUntilInclusive;
|
||||
this.callCount.set(0);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.callCount.set(0);
|
||||
}
|
||||
|
||||
public int callsSoFar() {
|
||||
return callCount.get();
|
||||
}
|
||||
|
||||
/** The call every service in this repo ultimately makes. */
|
||||
public String call() {
|
||||
int n = callCount.incrementAndGet();
|
||||
if (n <= failUntilInclusive) {
|
||||
throw new DownstreamUnavailableException("payment-gateway rejected call #" + n);
|
||||
}
|
||||
return "OK (call #" + n + ")";
|
||||
}
|
||||
|
||||
public static class DownstreamUnavailableException extends RuntimeException {
|
||||
public DownstreamUnavailableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.ankurm.resilience;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.resilience.annotation.EnableResilientMethods;
|
||||
|
||||
/**
|
||||
* Companion app for the ankurm.com post "Resilience4j on Spring Boot 4.1: what it's still for".
|
||||
*
|
||||
* <p>{@code @EnableResilientMethods} is required explicitly — Spring Boot 4.1's
|
||||
* autoconfigure jar carries no auto-configuration class for it (verified by grepping
|
||||
* spring-boot-autoconfigure-4.1.1.jar for "resilien": zero matches). The annotations
|
||||
* only start intercepting once this is present on a configuration class.
|
||||
*
|
||||
* See docs/01-two-resilience-stacks.md.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableResilientMethods
|
||||
public class ResilienceBoot4DemoApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ResilienceBoot4DemoApplication.class, args);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.ankurm.resilience.r4j;
|
||||
|
||||
import io.github.resilience4j.bulkhead.BulkheadFullException;
|
||||
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Resilience4j's semaphore Bulkhead, for direct comparison with Spring's
|
||||
* {@code @ConcurrencyLimit}. Config: resilience4j.bulkhead.instances.slowOp in application.yml,
|
||||
* maxWaitDuration: 100ms — a bounded wait, which {@code @ConcurrencyLimit}'s BLOCK policy does
|
||||
* not offer. See docs/04-concurrency-limit.md.
|
||||
*/
|
||||
@Service
|
||||
public class R4jBulkheadService {
|
||||
|
||||
@Bulkhead(name = "slowOp", fallbackMethod = "fallback")
|
||||
public String slowCall(String id, long sleepMillis) throws InterruptedException {
|
||||
Thread.sleep(sleepMillis);
|
||||
return "done:" + id;
|
||||
}
|
||||
|
||||
private String fallback(String id, long sleepMillis, BulkheadFullException ex) {
|
||||
return "REJECTED:" + id;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.ankurm.resilience.r4j;
|
||||
|
||||
import com.ankurm.resilience.FlakyDownstream;
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Resilience4j's circuit breaker: a state machine (CLOSED / OPEN / HALF_OPEN) that
|
||||
* remembers failure history *across calls*, sitting in front of the annotated method.
|
||||
*
|
||||
* Config lives in application.yml under resilience4j.circuitbreaker.instances.paymentService.
|
||||
* See docs/02-circuit-breaker.md.
|
||||
*/
|
||||
@Service
|
||||
public class R4jPaymentService {
|
||||
|
||||
private final FlakyDownstream downstream;
|
||||
|
||||
public R4jPaymentService(FlakyDownstream downstream) {
|
||||
this.downstream = downstream;
|
||||
}
|
||||
|
||||
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
|
||||
public String pay(String orderId) {
|
||||
return downstream.call();
|
||||
}
|
||||
|
||||
private String fallback(String orderId, Throwable ex) {
|
||||
return "FALLBACK for " + orderId + ": " + ex.getClass().getSimpleName() + " - " + ex.getMessage();
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.ankurm.resilience.r4j;
|
||||
|
||||
import com.ankurm.resilience.FlakyDownstream;
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Resilience4j's own {@code @Retry}, for direct comparison with core's {@code @Retryable}
|
||||
* (see {@link com.ankurm.resilience.springresilience.SpringRetryablePaymentService}). Config:
|
||||
* resilience4j.retry.instances.inventoryService in application.yml. See docs/03-spring-retryable.md.
|
||||
*/
|
||||
@Service
|
||||
public class R4jRetryService {
|
||||
|
||||
private final FlakyDownstream downstream;
|
||||
|
||||
public R4jRetryService(FlakyDownstream downstream) {
|
||||
this.downstream = downstream;
|
||||
}
|
||||
|
||||
@Retry(name = "inventoryService")
|
||||
public String checkStock(String productId) {
|
||||
return downstream.call();
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.ankurm.resilience.springresilience;
|
||||
|
||||
import org.springframework.resilience.annotation.ConcurrencyLimit;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* {@code @ConcurrencyLimit} is Spring Framework 7's version of what Resilience4j calls a
|
||||
* semaphore Bulkhead: cap the number of concurrent invocations of a method. Two policies:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code BLOCK} (the default) — callers past the limit queue and wait.</li>
|
||||
* <li>{@code REJECT} — callers past the limit get an
|
||||
* {@code org.springframework.resilience.InvocationRejectedException} immediately (a
|
||||
* {@link java.util.concurrent.RejectedExecutionException} subtype — verified with javap,
|
||||
* not documented in the annotation's Javadoc).</li>
|
||||
* </ul>
|
||||
*
|
||||
* Unlike Resilience4j's Bulkhead, there is no {@code maxWaitDuration} for the BLOCK policy —
|
||||
* a blocked caller waits until a slot frees, with no timeout of its own. Under the hood BLOCK
|
||||
* is implemented by extending {@code org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor},
|
||||
* a class that has shipped in Spring since the Spring 1.x era — repurposed, not reinvented.
|
||||
* See docs/04-concurrency-limit.md.
|
||||
*/
|
||||
@Service
|
||||
public class ConcurrencyLimitedService {
|
||||
|
||||
@ConcurrencyLimit(limit = 2)
|
||||
public String slowCallBlocking(String id, long sleepMillis) throws InterruptedException {
|
||||
Thread.sleep(sleepMillis);
|
||||
return "done:" + id;
|
||||
}
|
||||
|
||||
@ConcurrencyLimit(limit = 2, policy = ConcurrencyLimit.ThrottlePolicy.REJECT)
|
||||
public String slowCallRejecting(String id, long sleepMillis) throws InterruptedException {
|
||||
Thread.sleep(sleepMillis);
|
||||
return "done:" + id;
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.ankurm.resilience.springresilience;
|
||||
|
||||
import com.ankurm.resilience.FlakyDownstream;
|
||||
import org.springframework.resilience.annotation.Retryable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Spring Framework 7's own {@code @Retryable}: declarative retry with delay, multiplier
|
||||
* and jitter, no separate dependency. There is no {@code fallbackMethod} attribute —
|
||||
* unlike Resilience4j, exhausting retries just rethrows the last exception to the caller.
|
||||
*
|
||||
* And there is no circuit: each call to {@link #pay} starts its own retry loop from zero.
|
||||
* Nothing here remembers that the last five calls all failed. See docs/03-spring-retryable.md.
|
||||
*/
|
||||
@Service
|
||||
public class SpringRetryablePaymentService {
|
||||
|
||||
private final FlakyDownstream downstream;
|
||||
|
||||
public SpringRetryablePaymentService(FlakyDownstream downstream) {
|
||||
this.downstream = downstream;
|
||||
}
|
||||
|
||||
@Retryable(maxRetries = 3, delay = 200, multiplier = 2.0, timeUnit = TimeUnit.MILLISECONDS)
|
||||
public String pay(String orderId) {
|
||||
return downstream.call();
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-invocation trap: calling {@code pay(...)} from inside this same bean bypasses
|
||||
* the AOP proxy entirely, so no retry happens — the exact same trap Resilience4j's
|
||||
* annotations have via Spring AOP proxies (both are proxy-based interceptors).
|
||||
*/
|
||||
public String payViaSelfInvocation(String orderId) {
|
||||
return this.pay(orderId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
spring:
|
||||
application:
|
||||
name: resilience-boot4-demo
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,metrics,circuitbreakers,circuitbreakerevents
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
health:
|
||||
circuitbreakers:
|
||||
enabled: true
|
||||
|
||||
resilience4j:
|
||||
circuitbreaker:
|
||||
instances:
|
||||
paymentService:
|
||||
slidingWindowType: COUNT_BASED
|
||||
slidingWindowSize: 10
|
||||
minimumNumberOfCalls: 5
|
||||
failureRateThreshold: 50
|
||||
waitDurationInOpenState: 2s
|
||||
permittedNumberOfCallsInHalfOpenState: 2
|
||||
automaticTransitionFromOpenToHalfOpenEnabled: true
|
||||
recordExceptions:
|
||||
- com.ankurm.resilience.FlakyDownstream$DownstreamUnavailableException
|
||||
bulkhead:
|
||||
instances:
|
||||
slowOp:
|
||||
maxConcurrentCalls: 2
|
||||
maxWaitDuration: 100ms
|
||||
retry:
|
||||
instances:
|
||||
inventoryService:
|
||||
maxAttempts: 3
|
||||
waitDuration: 200ms
|
||||
enableExponentialBackoff: true
|
||||
exponentialBackoffMultiplier: 2
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.ankurm.resilience: INFO
|
||||
Reference in New Issue
Block a user