Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:34 +08:00
commit 390afca02b
220 changed files with 86013 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
# Resilience4j Configuration Reference
## Circuit Breaker Configuration
### Complete Properties List
```yaml
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true # Default: false
slidingWindowType: COUNT_BASED # COUNT_BASED or TIME_BASED
slidingWindowSize: 100 # Default: 100 (calls or seconds)
minimumNumberOfCalls: 10 # Default: 100
failureRateThreshold: 50 # Default: 50 (percentage)
slowCallRateThreshold: 100 # Default: 100 (percentage)
slowCallDurationThreshold: 60s # Default: 60000ms
waitDurationInOpenState: 60s # Default: 60000ms
automaticTransitionFromOpenToHalfOpenEnabled: false # Default: false
permittedNumberOfCallsInHalfOpenState: 10 # Default: 10
maxWaitDurationInHalfOpenState: 0s # Default: 0 (unlimited)
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
ignoreExceptions:
- java.lang.IllegalArgumentException
eventConsumerBufferSize: 100 # Default: 100
instances:
myService:
baseConfig: default
failureRateThreshold: 60
```
### Circuit Breaker States
1. **CLOSED**: Normal operation, calls pass through
2. **OPEN**: Circuit is open, calls immediately fail with `CallNotPermittedException`
3. **HALF_OPEN**: Testing if service recovered, allows limited test calls
4. **DISABLED**: Circuit breaker disabled, all calls pass through
5. **FORCED_OPEN**: Manually forced to open state for emergency situations
### Sliding Window Types
**COUNT_BASED** (Default)
- Aggregates outcome of last N calls
- Better for services with consistent traffic
- `slidingWindowSize` = number of calls to track
**TIME_BASED**
- Aggregates outcome of calls in last N seconds
- Better for services with variable traffic
- `slidingWindowSize` = time in seconds
## Retry Configuration
### Complete Properties List
```yaml
resilience4j:
retry:
configs:
default:
maxAttempts: 3 # Default: 3
waitDuration: 500ms # Default: 500ms
enableExponentialBackoff: false # Default: false
exponentialBackoffMultiplier: 2 # Default: 2
exponentialMaxWaitDuration: 10s # Default: no limit
enableRandomizedWait: false # Default: false
randomizedWaitFactor: 0.5 # Default: 0.5
retryExceptions:
- java.io.IOException
- org.springframework.web.client.ResourceAccessException
ignoreExceptions:
- java.lang.IllegalArgumentException
failAfterMaxAttempts: false # Default: false
eventConsumerBufferSize: 100 # Default: 100
instances:
myService:
baseConfig: default
maxAttempts: 5
```
### Exponential Backoff Example
```yaml
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2.0
exponentialMaxWaitDuration: 10s
```
Attempt waits:
- Attempt 1: 500ms
- Attempt 2: 1000ms (500 × 2)
- Attempt 3: 2000ms (1000 × 2)
- Attempt 4: 4000ms (2000 × 2)
- Attempt 5: 8000ms (4000 × 2)
- Maximum: 10000ms
## Rate Limiter Configuration
### Complete Properties List
```yaml
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 50 # Default: 50
limitRefreshPeriod: 500ns # Default: 500ns
timeoutDuration: 5s # Default: 5s
registerHealthIndicator: true # Default: false
allowHealthIndicatorToFail: true # Default: false
instances:
myService:
baseConfig: default
limitForPeriod: 10
limitRefreshPeriod: 1s
```
### Common Rate Limit Patterns
**10 requests per second**
```yaml
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 0s # Fail immediately if no permits
```
**100 requests per minute**
```yaml
limitForPeriod: 100
limitRefreshPeriod: 1m
timeoutDuration: 500ms # Wait up to 500ms for permit
```
**5 requests per second with queuing**
```yaml
limitForPeriod: 5
limitRefreshPeriod: 1s
timeoutDuration: 2s # Wait up to 2s for permit
```
## Bulkhead Configuration
### Semaphore Bulkhead
```yaml
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 25 # Default: 25
maxWaitDuration: 0ms # Default: 0
eventConsumerBufferSize: 100 # Default: 100
instances:
myService:
baseConfig: default
maxConcurrentCalls: 10
maxWaitDuration: 100ms
```
### Thread Pool Bulkhead
```yaml
resilience4j:
thread-pool-bulkhead:
configs:
default:
maxThreadPoolSize: 4 # Default: Runtime.availableProcessors()
coreThreadPoolSize: 2 # Default: Runtime.availableProcessors() - 1
queueCapacity: 100 # Default: 100
keepAliveDuration: 20ms # Default: 20ms
writableStackTraceEnabled: true # Default: true
instances:
myService:
baseConfig: default
maxThreadPoolSize: 8
coreThreadPoolSize: 4
queueCapacity: 200
```
## Time Limiter Configuration
### Complete Properties List
```yaml
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 1s # Default: 1s
cancelRunningFuture: true # Default: true
instances:
myService:
baseConfig: default
timeoutDuration: 3s
```
## Annotation Reference
### @CircuitBreaker
```java
@CircuitBreaker(
name = "serviceName", // Required: Instance name from config
fallbackMethod = "fallbackMethodName" // Optional: Fallback method name
)
// Fallback method signature
public String fallback(Long id, Exception ex) { }
```
### @Retry
```java
@Retry(
name = "serviceName", // Required: Instance name from config
fallbackMethod = "fallbackMethodName" // Optional: Fallback method name
)
```
### @RateLimiter
```java
@RateLimiter(
name = "serviceName",
fallbackMethod = "fallbackMethodName"
)
```
### @Bulkhead
```java
@Bulkhead(
name = "serviceName",
fallbackMethod = "fallbackMethodName",
type = Bulkhead.Type.SEMAPHORE // SEMAPHORE or THREADPOOL
)
```
### @TimeLimiter
```java
@TimeLimiter(
name = "serviceName",
fallbackMethod = "fallbackMethodName"
)
// Works only with CompletableFuture<T> or reactive types (Mono<T>, Flux<T>)
```
## Annotation Execution Order
When combining annotations on a method, execution order from outermost to innermost:
1. `@Retry`
2. `@CircuitBreaker`
3. `@RateLimiter`
4. `@TimeLimiter`
5. `@Bulkhead`
6. Actual method call
## Exception Reference
| Pattern | Exception | HTTP Status | Meaning |
|---------|-----------|-------------|---------|
| Circuit Breaker | `CallNotPermittedException` | 503 | Circuit is OPEN or FORCED_OPEN |
| Rate Limiter | `RequestNotPermitted` | 429 | No permits available |
| Bulkhead | `BulkheadFullException` | 503 | Bulkhead at capacity |
| Time Limiter | `TimeoutException` | 408 | Operation exceeded timeout |
## Programmatic Configuration
### Circuit Breaker
```java
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.slowCallDurationThreshold(Duration.ofSeconds(2))
.permittedNumberOfCallsInHalfOpenState(3)
.minimumNumberOfCalls(10)
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(100)
.recordExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(IllegalArgumentException.class)
.build();
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = registry.circuitBreaker("myService");
```
### Retry
```java
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.intervalFunction(IntervalFunction.ofExponentialBackoff(
Duration.ofMillis(500),
2.0
))
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(IllegalArgumentException.class)
.build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("myService");
```
## Actuator Endpoints
Access monitoring endpoints when management endpoints are enabled:
| Endpoint | Description |
|----------|-------------|
| `GET /actuator/circuitbreakers` | List all circuit breakers and states |
| `GET /actuator/circuitbreakerevents` | List circuit breaker events |
| `GET /actuator/retryevents` | List retry events |
| `GET /actuator/ratelimiters` | List rate limiters |
| `GET /actuator/bulkheads` | List bulkhead status |
| `GET /actuator/timelimiters` | List time limiters |
| `GET /actuator/metrics` | Custom resilience metrics |
## Micrometer Metrics
Resilience4j exposes the following metrics:
**Circuit Breaker Metrics**
- `resilience4j.circuitbreaker.calls{name, kind}`
- `resilience4j.circuitbreaker.state{name, state}`
- `resilience4j.circuitbreaker.failure.rate{name}`
- `resilience4j.circuitbreaker.slow.call.rate{name}`
**Retry Metrics**
- `resilience4j.retry.calls{name, kind}`
**Rate Limiter Metrics**
- `resilience4j.ratelimiter.available.permissions{name}`
- `resilience4j.ratelimiter.waiting_threads{name}`
**Bulkhead Metrics**
- `resilience4j.bulkhead.available.concurrent.calls{name}`
- `resilience4j.bulkhead.max.allowed.concurrent.calls{name}`
## Version Compatibility
| Resilience4j | Spring Boot | Java | Spring Framework |
|--------------|-------------|------|------------------|
| 2.2.x | 3.x | 17+ | 6.x |
| 2.1.x | 3.x | 17+ | 6.x |
| 2.0.x | 2.7.x | 8+ | 5.3.x |
| 1.7.x | 2.x | 8+ | 5.x |
## References
- [Resilience4j Official Documentation](https://resilience4j.readme.io/)
- [Spring Boot Integration Guide](https://resilience4j.readme.io/docs/getting-started-3)
- [Micrometer Metrics Guide](https://resilience4j.readme.io/docs/micrometer)

View File

@@ -0,0 +1,483 @@
# Resilience4j Real-World Examples
## E-Commerce Order Service
Complete example demonstrating all Resilience4j patterns in a microservices environment.
### Project Structure
```
order-service/
├── src/main/java/com/ecommerce/order/
│ ├── config/
│ │ ├── ResilienceConfig.java
│ │ └── RestTemplateConfig.java
│ ├── controller/
│ │ ├── OrderController.java
│ │ └── GlobalExceptionHandler.java
│ ├── service/
│ │ ├── OrderService.java
│ │ ├── PaymentService.java
│ │ ├── InventoryService.java
│ │ └── NotificationService.java
│ ├── domain/
│ │ ├── Order.java
│ │ ├── OrderStatus.java
│ │ └── Payment.java
└── src/main/resources/
└── application.yml
```
### Configuration
```yaml
server:
port: 8080
spring:
application:
name: order-service
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 30s
instances:
paymentService:
baseConfig: default
waitDurationInOpenState: 60s
inventoryService:
baseConfig: default
retry:
configs:
default:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
instances:
paymentService:
maxAttempts: 5
waitDuration: 1s
ratelimiter:
configs:
default:
limitForPeriod: 100
limitRefreshPeriod: 1s
instances:
emailService:
limitForPeriod: 10
limitRefreshPeriod: 1m
bulkhead:
configs:
default:
maxConcurrentCalls: 10
maxWaitDuration: 100ms
instances:
orderProcessing:
maxConcurrentCalls: 5
timelimiter:
configs:
default:
timeoutDuration: 3s
instances:
paymentService:
timeoutDuration: 5s
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: true
```
### Order Service Implementation
```java
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
private final NotificationService notificationService;
@Bulkhead(name = "orderProcessing", type = Bulkhead.Type.SEMAPHORE)
@Transactional
public Order processOrder(OrderRequest request) {
log.info("Processing order for customer: {}", request.getCustomerId());
Order order = createOrder(request);
try {
// Reserve inventory
inventoryService.reserveInventory(order);
// Process payment
String paymentId = paymentService.processPayment(order).get();
order = order.toBuilder().paymentId(paymentId).build();
// Send confirmation (async, best effort)
notificationService.sendOrderConfirmation(order);
log.info("Order processed successfully: {}", order.getId());
return order;
} catch (Exception ex) {
log.error("Order processing failed", ex);
compensateFailedOrder(order);
throw new OrderProcessingException("Failed to process order", ex);
}
}
private void compensateFailedOrder(Order order) {
try {
inventoryService.releaseInventory(order);
if (order.getPaymentId() != null) {
paymentService.refundPayment(order.getPaymentId());
}
} catch (Exception ex) {
log.error("Compensation failed", ex);
}
}
}
```
### Payment Service with Multiple Patterns
```java
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentClient paymentClient;
@CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback")
@Retry(name = "paymentService")
@TimeLimiter(name = "paymentService")
public CompletableFuture<String> processPayment(Order order) {
return CompletableFuture.supplyAsync(() -> {
Payment payment = Payment.builder()
.orderId(order.getId())
.amount(order.getTotalAmount())
.build();
PaymentResponse response = paymentClient.processPayment(payment);
if (!response.isSuccess()) {
throw new PaymentFailedException(response.getErrorMessage());
}
return response.getPaymentId();
});
}
private CompletableFuture<String> processPaymentFallback(
Order order, Exception ex) {
log.error("Payment processing failed for order: {}", order.getId(), ex);
throw new PaymentServiceUnavailableException(
"Payment service unavailable", ex);
}
@CircuitBreaker(name = "paymentService")
@Retry(name = "paymentService")
public void refundPayment(String paymentId) {
paymentClient.refundPayment(paymentId);
}
}
```
### Exception Handler
```java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CallNotPermittedException.class)
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public ErrorResponse handleCircuitOpen(CallNotPermittedException ex) {
log.error("Circuit breaker is open", ex);
return ErrorResponse.builder()
.code("SERVICE_UNAVAILABLE")
.message("Service is temporarily unavailable")
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
.build();
}
@ExceptionHandler(RequestNotPermitted.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ErrorResponse handleRateLimited(RequestNotPermitted ex) {
return ErrorResponse.builder()
.code("TOO_MANY_REQUESTS")
.message("Rate limit exceeded")
.status(HttpStatus.TOO_MANY_REQUESTS.value())
.build();
}
@ExceptionHandler(BulkheadFullException.class)
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public ErrorResponse handleBulkheadFull(BulkheadFullException ex) {
return ErrorResponse.builder()
.code("SERVICE_BUSY")
.message("Service at capacity")
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
.build();
}
@ExceptionHandler(TimeoutException.class)
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public ErrorResponse handleTimeout(TimeoutException ex) {
return ErrorResponse.builder()
.code("REQUEST_TIMEOUT")
.message("Request timed out")
.status(HttpStatus.REQUEST_TIMEOUT.value())
.build();
}
}
```
## Testing Patterns
### Unit Test for Circuit Breaker
```java
@SpringBootTest
class PaymentServiceCircuitBreakerTest {
@Autowired
private PaymentService paymentService;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@MockBean
private PaymentClient paymentClient;
private CircuitBreaker circuitBreaker;
@BeforeEach
void setup() {
circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService");
circuitBreaker.reset();
}
@Test
void shouldOpenCircuitAfterFailures() {
Order order = createTestOrder();
when(paymentClient.processPayment(any()))
.thenThrow(new RuntimeException("Service error"));
// Trigger failures to exceed threshold
for (int i = 0; i < 5; i++) {
try {
paymentService.processPayment(order).get();
} catch (Exception ignored) {}
}
assertThat(circuitBreaker.getState())
.isEqualTo(CircuitBreaker.State.OPEN);
// Next call should fail immediately
assertThatThrownBy(() -> paymentService.processPayment(order).get())
.hasRootCauseInstanceOf(PaymentServiceUnavailableException.class);
}
}
```
### Integration Test with WireMock
```java
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Test
void shouldRetryOnTransientFailure() {
// First two calls fail, third succeeds
stubFor(post("/payment/process")
.inScenario("Retry")
.whenScenarioStateIs(STARTED)
.willReturn(serverError())
.willSetStateTo("First Retry"));
stubFor(post("/payment/process")
.inScenario("Retry")
.whenScenarioStateIs("First Retry")
.willReturn(serverError())
.willSetStateTo("Second Retry"));
stubFor(post("/payment/process")
.inScenario("Retry")
.whenScenarioStateIs("Second Retry")
.willReturn(ok().withBody("{\"paymentId\":\"PAY-123\"}")));
Order order = orderService.processOrder(createOrderRequest());
assertThat(order.getPaymentId()).isEqualTo("PAY-123");
verify(exactly(3), postRequestedFor(urlEqualTo("/payment/process")));
}
}
```
## Advanced Scenarios
### Reactive WebFlux Example
```java
@Service
@RequiredArgsConstructor
public class ReactiveProductService {
private final WebClient webClient;
private final CircuitBreaker circuitBreaker;
private final Retry retry;
public Mono<Product> getProduct(String productId) {
return webClient.get()
.uri("/products/{id}", productId)
.retrieve()
.bodyToMono(Product.class)
.transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
.transformDeferred(RetryOperator.of(retry))
.onErrorResume(throwable ->
Mono.just(Product.unavailable(productId))
);
}
}
```
### Custom Resilience Configuration
```java
@Configuration
@Slf4j
public class ResilienceConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slowCallDurationThreshold(Duration.ofSeconds(2))
.permittedNumberOfCallsInHalfOpenState(3)
.minimumNumberOfCalls(5)
.slidingWindowSize(10)
.build();
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
// Register event consumer
registry.getEventPublisher()
.onEntryAdded(event ->
log.info("CircuitBreaker added: {}",
event.getAddedEntry().getName())
);
return registry;
}
@Bean
public RegistryEventConsumer<CircuitBreaker> circuitBreakerEventConsumer() {
return new RegistryEventConsumer<>() {
@Override
public void onEntryAddedEvent(EntryAddedEvent<CircuitBreaker> event) {
CircuitBreaker cb = event.getAddedEntry();
cb.getEventPublisher()
.onStateTransition(e ->
log.warn("CircuitBreaker {} state changed: {} -> {}",
cb.getName(),
e.getStateTransition().getFromState(),
e.getStateTransition().getToState())
)
.onError(e ->
log.error("CircuitBreaker {} error: {}",
cb.getName(),
e.getThrowable().getMessage())
);
}
@Override
public void onEntryRemovedEvent(EntryRemovedEvent<CircuitBreaker> event) {
log.info("CircuitBreaker removed: {}",
event.getRemovedEntry().getName());
}
@Override
public void onEntryReplacedEvent(EntryReplacedEvent<CircuitBreaker> event) {
log.info("CircuitBreaker replaced: {}",
event.getNewEntry().getName());
}
};
}
}
```
### Monitoring and Metrics
```java
@RestController
@RequestMapping("/api/monitoring")
@RequiredArgsConstructor
public class ResilienceMonitoringController {
private final CircuitBreakerRegistry circuitBreakerRegistry;
@GetMapping("/circuit-breakers")
public List<CircuitBreakerStatus> getStatus() {
return circuitBreakerRegistry.getAllCircuitBreakers().stream()
.map(this::toStatus)
.collect(Collectors.toList());
}
private CircuitBreakerStatus toStatus(CircuitBreaker cb) {
CircuitBreaker.Metrics metrics = cb.getMetrics();
return CircuitBreakerStatus.builder()
.name(cb.getName())
.state(cb.getState().name())
.failureRate(metrics.getFailureRate())
.slowCallRate(metrics.getSlowCallRate())
.numberOfBufferedCalls(metrics.getNumberOfBufferedCalls())
.numberOfFailedCalls(metrics.getNumberOfFailedCalls())
.numberOfSuccessfulCalls(metrics.getNumberOfSuccessfulCalls())
.build();
}
}
@Value
@Builder
class CircuitBreakerStatus {
String name;
String state;
float failureRate;
float slowCallRate;
int numberOfBufferedCalls;
int numberOfFailedCalls;
int numberOfSuccessfulCalls;
}
```
See testing-patterns.md for comprehensive testing strategies and configuration-reference.md for complete configuration options.

View File

@@ -0,0 +1,491 @@
# Resilience4j Testing Patterns
## Circuit Breaker Testing
### Testing State Transitions
```java
@SpringBootTest
class CircuitBreakerStateTest {
@Autowired
private PaymentService paymentService;
@MockBean
private RestTemplate restTemplate;
@Test
void shouldTransitionToOpenAfterFailures() {
// Simulate repeated failures
when(restTemplate.postForObject(anyString(), any(), eq(PaymentResponse.class)))
.thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR));
// Trigger failures to exceed threshold
for (int i = 0; i < 5; i++) {
assertThatThrownBy(() -> paymentService.processPayment(new PaymentRequest()))
.isInstanceOf(HttpServerErrorException.class);
}
// Circuit should be open - fallback executes
PaymentResponse response = paymentService.processPayment(new PaymentRequest());
assertThat(response.getStatus()).isEqualTo("PENDING");
}
@Test
void shouldExecuteFallbackWhenCircuitOpen() {
when(restTemplate.postForObject(anyString(), any(), eq(PaymentResponse.class)))
.thenThrow(new RuntimeException("Service unavailable"));
// Force failures to open circuit
for (int i = 0; i < 5; i++) {
try {
paymentService.processPayment(new PaymentRequest());
} catch (Exception ignored) {}
}
// Circuit is open, fallback provides response
PaymentResponse response = paymentService.processPayment(new PaymentRequest());
assertThat(response.getStatus()).isEqualTo("PENDING");
assertThat(response.getMessage()).contains("temporarily unavailable");
}
}
```
### Testing Circuit States Directly
```java
@SpringBootTest
class CircuitBreakerDirectStateTest {
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@Test
void shouldManuallyOpenAndCloseCircuit() {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService");
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
// Manually open circuit
circuitBreaker.transitionToOpenState();
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
// Manually close circuit
circuitBreaker.transitionToClosedState();
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
}
}
```
## Retry Testing
### Testing Retry Attempts
```java
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class RetryTest {
@Autowired
private OrderService orderService;
@Test
void shouldRetryOnTransientFailure() {
// Setup: First two calls fail, third succeeds
stubFor(post("/orders")
.inScenario("Retry Scenario")
.whenScenarioStateIs(STARTED)
.willReturn(serverError())
.willSetStateTo("First Failure"));
stubFor(post("/orders")
.inScenario("Retry Scenario")
.whenScenarioStateIs("First Failure")
.willReturn(serverError())
.willSetStateTo("Second Failure"));
stubFor(post("/orders")
.inScenario("Retry Scenario")
.whenScenarioStateIs("Second Failure")
.willReturn(ok().withBody("""
{"id":1,"status":"CREATED"}
""")));
Order order = orderService.createOrder(new OrderRequest());
assertThat(order.getId()).isEqualTo(1L);
assertThat(order.getStatus()).isEqualTo("CREATED");
// Verify exactly 3 calls were made
verify(exactly(3), postRequestedFor(urlEqualTo("/orders")));
}
@Test
void shouldThrowExceptionAfterMaxRetries() {
stubFor(post("/orders").willReturn(serverError()));
assertThatThrownBy(() -> orderService.createOrder(new OrderRequest()))
.isInstanceOf(Exception.class);
// Verify retry attempts (maxAttempts = 3)
verify(atLeast(3), postRequestedFor(urlEqualTo("/orders")));
}
}
```
## Rate Limiter Testing
### Testing Rate Limit Enforcement
```java
@SpringBootTest
class RateLimiterTest {
@Autowired
private NotificationService notificationService;
@Test
void shouldRejectRequestsExceedingRateLimit() {
// Configuration: 5 permits per second
// First 5 requests should succeed
for (int i = 0; i < 5; i++) {
notificationService.sendEmail(createEmailRequest(i));
}
// 6th request should fail immediately (no timeout)
assertThatThrownBy(() ->
notificationService.sendEmail(createEmailRequest(6))
).isInstanceOf(RequestNotPermitted.class);
}
@Test
void shouldAlowRequestsAfterWindowReset() throws InterruptedException {
// First batch of requests
for (int i = 0; i < 5; i++) {
notificationService.sendEmail(createEmailRequest(i));
}
// Wait for refresh period (1 second)
Thread.sleep(1100);
// Should succeed - window has reset
notificationService.sendEmail(createEmailRequest(5));
}
private EmailRequest createEmailRequest(int id) {
return EmailRequest.builder()
.to("user" + id + "@example.com")
.subject("Test " + id)
.build();
}
}
```
## Bulkhead Testing
### Testing Semaphore Bulkhead
```java
@SpringBootTest
class BulkheadSemaphoreTest {
@Autowired
private ReportService reportService;
@Test
void shouldLimitConcurrentCalls() {
// Configuration: maxConcurrentCalls = 5
CountDownLatch latch = new CountDownLatch(5);
List<CompletableFuture<Report>> futures = new ArrayList<>();
// Submit 5 concurrent calls
for (int i = 0; i < 5; i++) {
futures.add(CompletableFuture.supplyAsync(() -> {
latch.countDown();
return reportService.generateReport(new ReportRequest());
}));
}
// 6th call should be rejected
assertThatThrownBy(() ->
reportService.generateReport(new ReportRequest())
).isInstanceOf(BulkheadFullException.class);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
}
```
### Testing Thread Pool Bulkhead
```java
@SpringBootTest
class BulkheadThreadPoolTest {
@Autowired
private AnalyticsService analyticsService;
@Test
void shouldUseThreadPoolForAsync() {
// Configuration: threadPoolSize = 2, queueCapacity = 100
List<CompletableFuture<AnalyticsResult>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futures.add(analyticsService.runAnalytics(new AnalyticsRequest()));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (CompletableFuture<AnalyticsResult> future : futures) {
assertThat(future.join()).isNotNull();
}
}
}
```
## Time Limiter Testing
### Testing Timeout Enforcement
```java
@SpringBootTest
class TimeLimiterTest {
@Autowired
private SearchService searchService;
@Test
void shouldTimeoutExceededOperations() {
// Configuration: timeoutDuration = 1s
SearchQuery slowQuery = new SearchQuery();
slowQuery.setSimulatedDelay(Duration.ofSeconds(2));
assertThatThrownBy(() ->
searchService.search(slowQuery).get()
).hasCauseInstanceOf(TimeoutException.class);
}
@Test
void shouldReturnFallbackOnTimeout() {
SearchQuery slowQuery = new SearchQuery();
slowQuery.setSimulatedDelay(Duration.ofSeconds(2));
CompletableFuture<SearchResults> result = searchService.search(slowQuery);
SearchResults results = result.join();
assertThat(results).isNotNull();
assertThat(results.isTimedOut()).isTrue();
assertThat(results.getMessage()).contains("timed out");
}
}
```
## Fallback Method Signature Validation
### Correct Fallback Signatures
```java
@Service
public class PaymentService {
@CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
// method body
}
// CORRECT: Matches return type and parameters + Exception
private PaymentResponse paymentFallback(PaymentRequest request, Exception ex) {
// fallback logic
}
@Retry(name = "product")
public Product getProduct(String productId) {
// method body
}
// CORRECT: Can omit Exception parameter
private Product getProductFallback(String productId) {
// fallback logic
}
}
```
### Common Fallback Signature Errors
```java
@CircuitBreaker(name = "service", fallbackMethod = "fallback")
public String processData(Long id) { }
// WRONG: Missing parameter
public String fallback(Exception ex) { }
// WRONG: Wrong return type
public void fallback(Long id, Exception ex) { }
// WRONG: Wrong parameter type
public String fallback(String id, Exception ex) { }
// CORRECT:
public String fallback(Long id, Exception ex) { }
```
## Integration Testing Configuration
### Test Configuration Profile
```yaml
# application-test.yml
resilience4j:
circuitbreaker:
instances:
testService:
registerHealthIndicator: false
slidingWindowSize: 5
minimumNumberOfCalls: 3
failureRateThreshold: 50
waitDurationInOpenState: 100ms
retry:
instances:
testService:
maxAttempts: 2
waitDuration: 10ms
ratelimiter:
instances:
testService:
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 10ms
```
### Test Helper Methods
```java
@TestConfiguration
public class ResilienceTestConfig {
public static void openCircuitBreaker(CircuitBreaker circuitBreaker) {
circuitBreaker.transitionToOpenState();
}
public static void closeCircuitBreaker(CircuitBreaker circuitBreaker) {
circuitBreaker.transitionToClosedState();
}
public static void simulateFailures(
CircuitBreaker circuitBreaker,
int numberOfFailures) {
for (int i = 0; i < numberOfFailures; i++) {
try {
circuitBreaker.executeSupplier(() -> {
throw new RuntimeException("Simulated failure");
});
} catch (Exception ignored) {}
}
}
public static void resetCircuitBreaker(CircuitBreaker circuitBreaker) {
circuitBreaker.transitionToClosedState();
}
}
```
## Common Testing Mistakes
### Mistake 1: Not Waiting for Sliding Window
```java
// WRONG: Circuit might not open yet
for (int i = 0; i < 3; i++) {
try { service.call(); } catch (Exception e) {}
}
assertThat(circuit.getState()).isEqualTo(CircuitBreaker.State.OPEN); // May fail!
// CORRECT: Exceed minimumNumberOfCalls before checking
for (int i = 0; i < 5; i++) { // minimumNumberOfCalls = 5
try { service.call(); } catch (Exception e) {}
}
assertThat(circuit.getState()).isEqualTo(CircuitBreaker.State.OPEN);
```
### Mistake 2: Incorrect Fallback Method Access
```java
// WRONG: Fallback method is private, not accessible by AOP
@CircuitBreaker(name = "service", fallbackMethod = "fallback")
public String process(String data) { }
private String fallback(String data, Exception ex) { } // Private - won't work!
// CORRECT: Package-private or protected
protected String fallback(String data, Exception ex) { }
```
### Mistake 3: Not Mocking External Dependencies
```java
// WRONG: Circuit breaker might open due to real network calls
@SpringBootTest
class ServiceTest {
@Autowired
private ServiceWithCircuitBreaker service;
// Missing @MockBean for external service
@Test
void test() {
// Real network calls - unpredictable
}
}
// CORRECT: Mock external dependencies
@SpringBootTest
class ServiceTest {
@Autowired
private ServiceWithCircuitBreaker service;
@MockBean
private ExternalService externalService;
@Test
void test() {
when(externalService.call()).thenThrow(new RuntimeException());
// Predictable failure
}
}
```
## Performance Considerations
### Memory Usage in Tests
- **COUNT_BASED sliding window**: Stores last N call outcomes in memory
- **TIME_BASED sliding window**: May require more memory for high-throughput services
- Use smaller `slidingWindowSize` in tests to reduce memory footprint
### Timeout Configuration for Tests
```yaml
resilience4j:
timelimiter:
instances:
testService:
timeoutDuration: 2s # Longer timeout for slower CI/CD environments
circuitbreaker:
instances:
testService:
waitDurationInOpenState: 100ms # Shorter for faster test execution
```
### Avoiding Test Flakiness
- Set deterministic timeouts based on CI/CD environment
- Use `@ActiveProfiles("test")` for test-specific configurations
- Reset circuit breaker state between tests when needed
- Mock external services consistently