0%
Circuit Breaker avec Resilience4J

Circuit Breaker

Circuit Breaker avec Resilience4J

10-15 min

Circuit Breaker avec Resilience4J

Le pattern Circuit Breaker est essentiel pour gérer la résilience dans les architectures microservices. Resilience4J est une bibliothèque légère et fonctionnelle qui implémente ce pattern, ainsi que d’autres patterns de résilience.

1. Configuration de Base

1.1 Ajout des Dépendances

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

1.2 Configuration du Circuit Breaker

# application.yml
resilience4j:
  circuitbreaker:
    instances:
      productService:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 5000
        permittedNumberOfCallsInHalfOpenState: 3
        eventConsumerBufferSize: 10
  ratelimiter:
    instances:
      productService:
        limitForPeriod: 100
        limitRefreshPeriod: 1s
        timeoutDuration: 0
  retry:
    instances:
      productService:
        maxAttempts: 3
        waitDuration: 100
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        exponentialBackoffMaxDuration: 600
        retryExceptions:
          - org.springframework.web.client.RestClientException

2. Implémentation du Circuit Breaker

2.1 Service avec Circuit Breaker

@Service
@Slf4j
public class ProductService {
    private final CircuitBreaker circuitBreaker;
    private final RateLimiter rateLimiter;
    private final Retry retry;
    private final ProductRepository productRepository;

    public ProductService(
            CircuitBreakerRegistry circuitBreakerRegistry,
            RateLimiterRegistry rateLimiterRegistry,
            RetryRegistry retryRegistry,
            ProductRepository productRepository) {
        this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("productService");
        this.rateLimiter = rateLimiterRegistry.rateLimiter("productService");
        this.retry = retryRegistry.retry("productService");
        this.productRepository = productRepository;
    }

    public Product getProduct(Long id) {
        return circuitBreaker.run(
            () -> retry.executeSupplier(
                () -> rateLimiter.executeSupplier(
                    () -> productRepository.findById(id)
                        .orElseThrow(() -> new ProductNotFoundException(id))
                )
            ),
            throwable -> {
                log.error("Error fetching product: {}", throwable.getMessage());
                return new Product(id, "Fallback Product", "Service unavailable", 
                    BigDecimal.ZERO, 0);
            }
        );
    }
}

2.2 Configuration des Événements

@Configuration
public class CircuitBreakerConfig {
    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .slidingWindowSize(10)
            .failureRateThreshold(50)
            .waitDurationInOpenState(Duration.ofSeconds(5))
            .permittedNumberOfCallsInHalfOpenState(3)
            .build();

        return CircuitBreakerRegistry.of(config);
    }

    @PostConstruct
    public void postConstruct(CircuitBreakerRegistry registry) {
        CircuitBreaker circuitBreaker = registry.circuitBreaker("productService");
        circuitBreaker.getEventPublisher()
            .onStateTransition(event -> {
                log.info("Circuit Breaker state changed from {} to {}",
                    event.getStateTransition().getFromState(),
                    event.getStateTransition().getToState());
            });
    }
}

3. Gestion des Erreurs

3.1 Exceptions Personnalisées

public class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(Long id) {
        super("Product not found with id: " + id);
    }
}

public class ServiceUnavailableException extends RuntimeException {
    public ServiceUnavailableException(String message) {
        super(message);
    }
}

3.2 Gestionnaire d’Erreurs Global

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleProductNotFound(ProductNotFoundException ex) {
        ErrorResponse response = new ErrorResponse(
            "PRODUCT_NOT_FOUND",
            ex.getMessage()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }

    @ExceptionHandler(ServiceUnavailableException.class)
    public ResponseEntity<ErrorResponse> handleServiceUnavailable(ServiceUnavailableException ex) {
        ErrorResponse response = new ErrorResponse(
            "SERVICE_UNAVAILABLE",
            ex.getMessage()
        );
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response);
    }
}

4. Monitoring et Métriques

4.1 Configuration Actuator

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always

4.2 Métriques Personnalisées

@Component
public class CircuitBreakerMetrics {
    private final MeterRegistry registry;
    private final CircuitBreaker circuitBreaker;

    public CircuitBreakerMetrics(MeterRegistry registry, CircuitBreaker circuitBreaker) {
        this.registry = registry;
        this.circuitBreaker = circuitBreaker;
        registerMetrics();
    }

    private void registerMetrics() {
        Gauge.builder("circuit.breaker.state", circuitBreaker, this::getStateValue)
            .description("Circuit breaker state")
            .register(registry);

        Counter.builder("circuit.breaker.calls")
            .description("Number of calls")
            .tag("result", "success")
            .register(registry);

        Counter.builder("circuit.breaker.calls")
            .description("Number of calls")
            .tag("result", "failure")
            .register(registry);
    }

    private double getStateValue(CircuitBreaker cb) {
        switch (cb.getState()) {
            case CLOSED: return 0;
            case OPEN: return 1;
            case HALF_OPEN: return 2;
            default: return -1;
        }
    }
}

5. Tests

5.1 Test Unitaire

@SpringBootTest
class ProductServiceTest {
    @Autowired
    private ProductService productService;

    @MockBean
    private ProductRepository productRepository;

    @Test
    void whenCircuitBreakerIsOpen_thenFallbackIsReturned() {
        // Arrange
        when(productRepository.findById(1L))
            .thenThrow(new RuntimeException("Service unavailable"));

        // Act
        Product product = productService.getProduct(1L);

        // Assert
        assertEquals("Fallback Product", product.getName());
        assertEquals(BigDecimal.ZERO, product.getPrice());
    }
}

5.2 Test d’Intégration

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CircuitBreakerIntegrationTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void whenServiceIsUnavailable_thenCircuitBreakerIsTriggered() {
        // Act
        ResponseEntity<Product> response = restTemplate.getForEntity(
            "/api/products/1", Product.class);

        // Assert
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertEquals("Fallback Product", response.getBody().getName());
    }
}

Conclusion

Le Circuit Breaker avec Resilience4J offre plusieurs avantages :

  • Protection contre les pannes en cascade
  • Gestion automatique des états (ouvert, fermé, semi-ouvert)
  • Métriques et monitoring intégrés
  • Fallback automatique en cas d’échec
  • Configuration flexible et extensible

Dans le prochain tutoriel, nous verrons comment implémenter le tracing distribué avec Sleuth et Zipkin pour suivre les requêtes à travers nos microservices.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !