Resilience4J for fault tolerance,

<

Resilience4j

Guide Complet et Détaillé - Explications Approfondies

Introduction à Resilience4j

Qu'est-ce que Resilience4j ?

Resilience4j est une bibliothèque de tolérance aux pannes légère et facile à utiliser conçue pour les applications Java 8 et fonctionnelles. Inspirée par Netflix Hystrix, elle est conçue pour être modulaire et utilisable avec des bibliothèques fonctionnelles comme Vavr.

Pourquoi Resilience4j ?

  • Léger : Pas de dépendances lourdes comme Hystrix
  • Modulaire : Chaque pattern de résilience est une bibliothèque séparée
  • Fonctionnel : Conçu pour la programmation fonctionnelle avec Vavr
  • Reactive : Supporte les flux réactifs (Reactor, RxJava)
  • Spring Boot : Intégration native avec Spring Boot
  • Monitoring : Métriques et dashboards intégrés

Architecture de Resilience4j

Architecture modulaire de Resilience4j :

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   CircuitBreaker│    │      Retry      │    │   RateLimiter   │
└─────────────────┘    └─────────────────┘    └─────────────────┘
        │                      │                      │
        └──────────────────────┼──────────────────────┘
                               │
                    ┌─────────▼─────────┐    ┌─────────────────┐
                    │    Bulkhead       │    │   TimeLimiter   │
                    └───────────────────┘    └─────────────────┘
                               │                      │
                               └──────────────────────┘
                                          │
                              ┌──────────▼──────────┐
                              │      Cache          │
                              └─────────────────────┘
                    

Comparaison avec Hystrix

Hystrix vs Resilience4j

Caractéristique Hystrix Resilience4j
Architecture Monolithique Modulaire
Dépendances Lourdes (Archaius, RxJava, etc.) Légères
Support Reactive Limité Natif
Configuration Complexe Simple et flexible
Maintenance Mode maintenance Actif

Migration de Hystrix vers Resilience4j

  • Plus performant : Moins de surcharge mémoire et CPU
  • Plus flexible : Composants indépendants utilisables séparément
  • Meilleure intégration : Avec les frameworks modernes
  • Support actif : Développement continu et communauté active

Concepts Fondamentaux de Resilience4j

Philosophie Fonctionnelle

Programmation Déclarative

Resilience4j suit une approche déclarative où les patterns de résilience sont appliqués comme des décorateurs autour du code métier.

1

Approche fonctionnelle :

// Approche fonctionnelle avec Resilience4j
Supplier<String> decoratedSupplier = Decorators.ofSupplier(supplier)
    .withCircuitBreaker(circuitBreaker)
    .withRetry(retry)
    .withTimeLimiter(timeLimiter)
    .decorate();

String result = Try.ofSupplier(decoratedSupplier)
    .recover(throwable -> "Fallback value");
2

Approche impérative :

// Approche impérative avec Resilience4j
String result;
try {
    result = circuitBreaker.executeSupplier(() -> {
        return retry.executeSupplier(() -> {
            return timeLimiter.executeSupplier(supplier);
        });
    });
} catch (Exception e) {
    result = "Fallback value";
}

Composants Principaux

  • CircuitBreaker : Empêche les appels répétés vers un service défaillant
  • Retry : Retente automatiquement les opérations échouées
  • RateLimiter : Limite le nombre d'appels dans une période donnée
  • Bulkhead : Isole les ressources pour éviter la contention
  • TimeLimiter : Limite le temps d'exécution des appels
  • Cache : Met en cache les résultats pour améliorer les performances

États des Patterns

États du Circuit Breaker

Le Circuit Breaker a trois états principaux :

État CLOSED
  • Normal : Les requêtes passent normalement
  • Monitoring : Suivi des erreurs et latences
  • Transition : Passe à OPEN si seuil d'erreurs atteint
État OPEN
  • Bloqué : Toutes les requêtes sont immédiatement rejetées
  • Timeout : Attend un certain temps avant de passer à HALF_OPEN
  • Protection : Protège le service défaillant de plus de requêtes
État HALF_OPEN
  • Test : Permet un nombre limité de requêtes de test
  • Validation : Vérifie si le service est rétabli
  • Transition : Retour à CLOSED si succès, reste OPEN si échec

États du Rate Limiter

Le Rate Limiter gère le débit des requêtes :

État ACTIVE
  • Normal : Autorise les requêtes selon le débit configuré
  • Monitoring : Suit le nombre de requêtes par période
  • Limitation : Rejette les requêtes excédentaires

Circuit Breaker

Configuration de Base

1

Création d'un Circuit Breaker :

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import java.time.Duration;

// Configuration du Circuit Breaker
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)                    // 50% d'erreurs déclenche l'ouverture
    .slowCallRateThreshold(100)                  // 100% de calls lents déclenchent l'ouverture
    .slowCallDurationThreshold(Duration.ofSeconds(2)) // Temps considéré comme lent
    .waitDurationInOpenState(Duration.ofSeconds(10))  // Temps d'attente en OPEN
    .permittedNumberOfCallsInHalfOpenState(3)         // 3 appels en HALF_OPEN
    .minimumNumberOfCalls(10)                         // Minimum 10 calls pour évaluer
    .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
    .slidingWindowSize(100)                           // Fenêtre de 100 calls ou 100 secondes
    .recordExceptions(IOException.class, TimeoutException.class) // Exceptions à enregistrer
    .recordFailure(throwable -> throwable instanceof BusinessException) // Prédicat personnalisé
    .build();

// Création du Circuit Breaker
CircuitBreaker circuitBreaker = CircuitBreaker.of("backendService", config);

Explication des Propriétés

  • failureRateThreshold(50) :

    Pourcentage d'erreurs qui déclenche l'ouverture du circuit (50% par défaut).

  • slowCallRateThreshold(100) :

    Pourcentage de calls lents qui déclenche l'ouverture (100% par défaut).

  • slowCallDurationThreshold(2s) :

    Durée au-delà de laquelle un call est considéré comme lent (60s par défaut).

  • waitDurationInOpenState(10s) :

    Temps d'attente en état OPEN avant passage à HALF_OPEN (60s par défaut).

  • permittedNumberOfCallsInHalfOpenState(3) :

    Nombre de calls autorisés en HALF_OPEN (10 par défaut).

  • minimumNumberOfCalls(10) :

    Nombre minimum de calls pour évaluer le circuit (100 par défaut).

2

Utilisation du Circuit Breaker :

// Méthode à protéger
Supplier<String> supplier = () -> externalService.call();

// Application du Circuit Breaker
Supplier<String> decoratedSupplier = CircuitBreaker
    .decorateSupplier(circuitBreaker, supplier);

// Exécution avec fallback
String result = Try.ofSupplier(decoratedSupplier)
    .recover(throwable -> "Fallback result");

// Ou avec gestion d'exceptions spécifique
try {
    String result = decoratedSupplier.get();
} catch (CallNotPermittedException e) {
    // Circuit ouvert - service temporairement indisponible
    return "Service temporarily unavailable";
} catch (Exception e) {
    // Autres erreurs
    return "Error occurred: " + e.getMessage();
}

Configuration Avancée

1

Configuration avec fenêtre glissante :

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
    .slidingWindowSize(100)           // 100 derniers appels
    .failureRateThreshold(30)         // 30% d'erreurs
    .waitDurationInOpenState(Duration.ofMinutes(1))
    .permittedNumberOfCallsInHalfOpenState(5)
    .build();
2

Configuration avec prédicats personnalisés :

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .recordFailure(throwable -> {
        // Enregistrer comme échec si :
        // - Exception réseau
        // - Timeout
        // - Erreurs métier spécifiques
        return throwable instanceof IOException || 
               throwable instanceof TimeoutException ||
               (throwable instanceof BusinessException && 
                ((BusinessException) throwable).isRetryable());
    })
    .ignoreExceptions(ValidationException.class) // Ignorer certaines exceptions
    .build();
3

Gestion des appels lents :

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .slowCallRateThreshold(80)        // 80% de calls lents
    .slowCallDurationThreshold(Duration.ofSeconds(5)) // Plus de 5s = lent
    .waitDurationInOpenState(Duration.ofSeconds(60))
    .build();

Événements et Monitoring

1

Écoute des événements :

// Écoute des événements du Circuit Breaker
circuitBreaker.getEventPublisher()
    .onSuccess(event -> logger.info("Call succeeded"))
    .onError(event -> logger.error("Call failed: " + event.getThrowable().getMessage()))
    .onStateTransition(event -> {
        CircuitBreaker.State from = event.getStateTransition().getFromState();
        CircuitBreaker.State to = event.getStateTransition().getToState();
        logger.info("State transition: {} -> {}", from, to);
    })
    .onSlowCallRateExceeded(event -> logger.warn("Slow call rate exceeded"))
    .onFailureRateExceeded(event -> logger.warn("Failure rate exceeded"));
2

Récupération des métriques :

// Récupération des métriques
CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();
int numberOfSuccessfulCalls = metrics.getNumberOfSuccessfulCalls();
int numberOfFailedCalls = metrics.getNumberOfFailedCalls();
int numberOfNotPermittedCalls = metrics.getNumberOfNotPermittedCalls();
float failureRate = metrics.getFailureRate();
float slowCallRate = metrics.getSlowCallRate();

logger.info("Success: {}, Failed: {}, Not permitted: {}, Failure rate: {}%, Slow call rate: {}%",
    numberOfSuccessfulCalls, numberOfFailedCalls, numberOfNotPermittedCalls, 
    failureRate, slowCallRate);

Retry

Configuration de Base

1

Création d'une configuration Retry :

import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import java.time.Duration;

// Configuration du Retry
RetryConfig config = RetryConfig.custom()
    .maxAttempts(3)                           // 3 tentatives maximum
    .waitDuration(Duration.ofSeconds(1))      // 1 seconde entre chaque tentative
    .retryExceptions(IOException.class, TimeoutException.class) // Exceptions à retenter
    .retryOnResult(response -> response == null || response.isEmpty()) // Retenter sur résultat nul
    .build();

// Création du Retry
Retry retry = Retry.of("backendService", config);

Explication des Propriétés

  • maxAttempts(3) :

    Nombre maximum de tentatives (1 tentative initiale + 2 retries).

  • waitDuration(1s) :

    Durée d'attente fixe entre les tentatives.

  • retryExceptions :

    Liste des exceptions qui déclenchent un retry.

  • retryOnResult :

    Prédicat pour retenter sur certains résultats.

2

Utilisation du Retry :

// Méthode à retenter
Supplier<String> supplier = () -> externalService.call();

// Application du Retry
Supplier<String> decoratedSupplier = Retry
    .decorateSupplier(retry, supplier);

// Exécution avec fallback
String result = Try.ofSupplier(decoratedSupplier)
    .recover(throwable -> "Fallback result");

// Ou avec gestion d'exceptions
try {
    String result = decoratedSupplier.get();
} catch (Exception e) {
    logger.error("All retry attempts failed", e);
    return "Default value";
}

Stratégies d'Attente

1

Attente fixe :

RetryConfig config = RetryConfig.custom()
    .maxAttempts(3)
    .waitDuration(Duration.ofSeconds(2)) // 2 secondes fixes entre chaque tentative
    .build();
2

Backoff exponentiel :

RetryConfig config = RetryConfig.custom()
    .maxAttempts(5)
    .intervalFunction(IntervalFunction.ofExponentialBackoff(
        Duration.ofSeconds(1),    // Durée initiale
        2.0                       // Facteur multiplicateur
    ))
    .build();

// Résultat : 1s, 2s, 4s, 8s, 16s
3

Backoff avec jitter :

RetryConfig config = RetryConfig.custom()
    .maxAttempts(4)
    .intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
        Duration.ofSeconds(1),    // Durée initiale
        2.0,                      // Facteur multiplicateur
        0.5                       // Facteur de jitter (±50%)
    ))
    .build();

// Résultat : 1s ±50%, 2s ±50%, 4s ±50%, 8s ±50%
4

Prédicats personnalisés :

RetryConfig config = RetryConfig.custom()
    .maxAttempts(3)
    .waitDuration(Duration.ofSeconds(1))
    .retryOnException(throwable -> {
        // Retenter seulement sur certaines conditions
        if (throwable instanceof IOException) {
            return true; // Toujours retenter les IOException
        }
        if (throwable instanceof BusinessException) {
            BusinessException be = (BusinessException) throwable;
            return be.isRetryable(); // Retenter selon la logique métier
        }
        return false; // Ne pas retenter les autres exceptions
    })
    .retryOnResult(result -> {
        // Retenter sur certains résultats
        if (result instanceof Response) {
            Response response = (Response) result;
            return response.getStatusCode() == 503; // Service Unavailable
        }
        return false;
    })
    .build();

Événements et Monitoring

1

Écoute des événements :

// Écoute des événements du Retry
retry.getEventPublisher()
    .onRetry(event -> {
        logger.warn("Retry attempt {} for {} due to: {}", 
            event.getNumberOfRetryAttempts(), 
            event.getName(), 
            event.getLastThrowable().getMessage());
    })
    .onSuccess(event -> {
        logger.info("Retry successful after {} attempts", 
            event.getNumberOfRetryAttempts());
    })
    .onError(event -> {
        logger.error("Retry failed after {} attempts", 
            event.getNumberOfRetryAttempts(), 
            event.getLastError());
    });
2

Récupération des métriques :

// Récupération des métriques
Retry.Metrics metrics = retry.getMetrics();
long successfulCallsWithoutRetry = metrics.getNumberOfSuccessfulCallsWithoutRetryAttempt();
long successfulCallsWithRetry = metrics.getNumberOfSuccessfulCallsWithRetryAttempt();
long failedCallsWithoutRetry = metrics.getNumberOfFailedCallsWithoutRetryAttempt();
long failedCallsWithRetry = metrics.getNumberOfFailedCallsWithRetryAttempt();

logger.info("Successful without retry: {}, with retry: {}, Failed without retry: {}, with retry: {}",
    successfulCallsWithoutRetry, successfulCallsWithRetry,
    failedCallsWithoutRetry, failedCallsWithRetry);

Rate Limiter

Configuration de Base

1

Création d'un Rate Limiter :

import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import java.time.Duration;

// Configuration du Rate Limiter
RateLimiterConfig config = RateLimiterConfig.custom()
    .limitRefreshPeriod(Duration.ofSeconds(1))  // Période de rafraîchissement
    .limitForPeriod(10)                         // 10 appels par période
    .timeoutDuration(Duration.ofSeconds(5))     // Timeout d'attente
    .build();

// Création du Rate Limiter
RateLimiter rateLimiter = RateLimiter.of("backendService", config);

Explication des Propriétés

  • limitRefreshPeriod(1s) :

    Période pendant laquelle les permissions sont réinitialisées.

  • limitForPeriod(10) :

    Nombre maximum de permissions par période (10 appels/seconde).

  • timeoutDuration(5s) :

    Durée maximale d'attente pour obtenir une permission.

2

Utilisation du Rate Limiter :

// Méthode à limiter
Supplier<String> supplier = () -> externalService.call();

// Application du Rate Limiter
Supplier<String> decoratedSupplier = RateLimiter
    .decorateSupplier(rateLimiter, supplier);

// Exécution avec gestion du timeout
Try<String> result = Try.ofSupplier(decoratedSupplier)
    .recover(throwable -> {
        if (throwable instanceof RequestNotPermitted) {
            return "Rate limit exceeded";
        }
        return "Error occurred";
    });

// Ou avec gestion explicite
try {
    String result = decoratedSupplier.get();
} catch (RequestNotPermitted e) {
    logger.warn("Rate limit exceeded for backend service");
    return "Service temporarily unavailable due to rate limit";
} catch (Exception e) {
    logger.error("Error calling backend service", e);
    return "Error occurred";
}

Configuration Avancée

1

Limites différentes selon les périodes :

// 100 appels par minute
RateLimiterConfig config = RateLimiterConfig.custom()
    .limitRefreshPeriod(Duration.ofMinutes(1))
    .limitForPeriod(100)
    .timeoutDuration(Duration.ofSeconds(10))
    .build();
2

Timeouts personnalisés :

// Timeout court pour les services rapides
RateLimiterConfig fastServiceConfig = RateLimiterConfig.custom()
    .limitRefreshPeriod(Duration.ofSeconds(1))
    .limitForPeriod(50)
    .timeoutDuration(Duration.ofMillis(100)) // Timeout court
    .build();

// Timeout long pour les services lents
RateLimiterConfig slowServiceConfig = RateLimiterConfig.custom()
    .limitRefreshPeriod(Duration.ofSeconds(10))
    .limitForPeriod(5)
    .timeoutDuration(Duration.ofSeconds(30)) // Timeout long
    .build();
3

Configuration par client :

// Rate limiter différent selon le type de client
RateLimiterConfig premiumConfig = RateLimiterConfig.custom()
    .limitRefreshPeriod(Duration.ofSeconds(1))
    .limitForPeriod(100) // Plus de permissions pour les clients premium
    .timeoutDuration(Duration.ofSeconds(5))
    .build();

RateLimiterConfig freeConfig = RateLimiterConfig.custom()
    .limitRefreshPeriod(Duration.ofSeconds(1))
    .limitForPeriod(10)  // Moins de permissions pour les clients gratuits
    .timeoutDuration(Duration.ofSeconds(5))
    .build();

Événements et Monitoring

1

Écoute des événements :

// Écoute des événements du Rate Limiter
rateLimiter.getEventPublisher()
    .onSuccess(event -> {
        logger.debug("Rate limiter permission granted for {}", event.getName());
    })
    .onFailure(event -> {
        logger.warn("Rate limiter permission denied for {}", event.getName());
    });
2

Récupération des métriques :

// Récupération des métriques
RateLimiter.Metrics metrics = rateLimiter.getMetrics();
int availablePermissions = metrics.getAvailablePermissions();
int numberOfWaitingThreads = metrics.getNumberOfWaitingThreads();

logger.info("Available permissions: {}, Waiting threads: {}",
    availablePermissions, numberOfWaitingThreads);

Bulkhead

Configuration de Base

Deux Types de Bulkhead

  • SemaphoreBulkhead : Limite le nombre de threads concurrents
  • ThreadPoolBulkhead : Isole les appels dans un pool de threads dédié
1

Semaphore Bulkhead :

import io.github.resilience4j.bulkhead.Bulkhead;
import io.github.resilience4j.bulkhead.BulkheadConfig;

// Configuration du Semaphore Bulkhead
BulkheadConfig config = BulkheadConfig.custom()
    .maxConcurrentCalls(10)        // Maximum 10 appels concurrents
    .maxWaitDuration(Duration.ofSeconds(5)) // Timeout d'attente
    .build();

// Création du Bulkhead
Bulkhead bulkhead = Bulkhead.of("backendService", config);
2

ThreadPool Bulkhead :

import io.github.resilience4j.bulkhead.ThreadPoolBulkhead;
import io.github.resilience4j.bulkhead.ThreadPoolBulkheadConfig;

// Configuration du ThreadPool Bulkhead
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
    .coreThreadPoolSize(2)         // 2 threads de base
    .maxThreadPoolSize(4)          // Maximum 4 threads
    .queueCapacity(20)             // File d'attente de 20
    .keepAliveDuration(Duration.ofMinutes(1)) // Durée de vie des threads
    .build();

// Création du ThreadPool Bulkhead
ThreadPoolBulkhead threadPoolBulkhead = ThreadPoolBulkhead.of("backendService", config);

Explication des Propriétés

  • maxConcurrentCalls(10) :

    Nombre maximum d'appels concurrents autorisés.

  • maxWaitDuration(5s) :

    Durée maximale d'attente pour obtenir une permission.

  • coreThreadPoolSize(2) :

    Nombre de threads de base dans le pool.

  • maxThreadPoolSize(4) :

    Nombre maximum de threads dans le pool.

  • queueCapacity(20) :

    Taille de la file d'attente pour les tâches.

Utilisation des Bulkheads

1

Utilisation du Semaphore Bulkhead :

// Méthode à isoler
Supplier<String> supplier = () -> externalService.call();

// Application du Semaphore Bulkhead
Supplier<String> decoratedSupplier = Bulkhead
    .decorateSupplier(bulkhead, supplier);

// Exécution avec gestion des exceptions
try {
    String result = decoratedSupplier.get();
} catch (BulkheadFullException e) {
    logger.warn("Bulkhead is full, rejecting call");
    return "Service temporarily unavailable due to high load";
} catch (Exception e) {
    logger.error("Error calling backend service", e);
    return "Error occurred";
}
2

Utilisation du ThreadPool Bulkhead :

// Méthode à isoler (doit retourner CompletableFuture)
Supplier<CompletionStage<String>> supplier = () -> 
    CompletableFuture.supplyAsync(() -> externalService.call());

// Application du ThreadPool Bulkhead
Supplier<CompletionStage<String>> decoratedSupplier = ThreadPoolBulkhead
    .decorateSupplier(threadPoolBulkhead, supplier);

// Exécution avec gestion des exceptions
try {
    CompletionStage<String> future = decoratedSupplier.get();
    return future.toCompletableFuture().get(30, TimeUnit.SECONDS);
} catch (BulkheadFullException e) {
    logger.warn("ThreadPool bulkhead is full, rejecting call");
    return "Service temporarily unavailable due to high load";
} catch (Exception e) {
    logger.error("Error calling backend service", e);
    return "Error occurred";
}

Configuration Avancée

1

Bulkhead par service :

// Bulkhead pour les services critiques
BulkheadConfig criticalConfig = BulkheadConfig.custom()
    .maxConcurrentCalls(50)        // Plus de permissions pour les services critiques
    .maxWaitDuration(Duration.ofMillis(100))
    .build();

// Bulkhead pour les services non critiques
BulkheadConfig nonCriticalConfig = BulkheadConfig.custom()
    .maxConcurrentCalls(5)         // Moins de permissions pour les services non critiques
    .maxWaitDuration(Duration.ofSeconds(1))
    .build();
2

ThreadPool Bulkhead optimisé :

ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
    .coreThreadPoolSize(Runtime.getRuntime().availableProcessors()) // Threads selon CPU
    .maxThreadPoolSize(Runtime.getRuntime().availableProcessors() * 2)
    .queueCapacity(100)
    .keepAliveDuration(Duration.ofMinutes(2))
    .build();

Événements et Monitoring

1

Écoute des événements :

// Écoute des événements du Bulkhead
bulkhead.getEventPublisher()
    .onCallPermitted(event -> {
        logger.debug("Bulkhead call permitted for {}", event.getName());
    })
    .onCallRejected(event -> {
        logger.warn("Bulkhead call rejected for {}", event.getName());
    });

// Écoute des événements du ThreadPool Bulkhead
threadPoolBulkhead.getEventPublisher()
    .onCallPermitted(event -> {
        logger.debug("ThreadPool bulkhead call permitted for {}", event.getName());
    })
    .onCallRejected(event -> {
        logger.warn("ThreadPool bulkhead call rejected for {}", event.getName());
    });
2

Récupération des métriques :

// Récupération des métriques du Semaphore Bulkhead
Bulkhead.Metrics bulkheadMetrics = bulkhead.getMetrics();
int availableConcurrentCalls = bulkheadMetrics.getAvailableConcurrentCalls();
int maxAllowedConcurrentCalls = bulkheadMetrics.getMaxAllowedConcurrentCalls();

logger.info("Available concurrent calls: {}, Max allowed: {}",
    availableConcurrentCalls, maxAllowedConcurrentCalls);

// Récupération des métriques du ThreadPool Bulkhead
ThreadPoolBulkhead.Metrics threadPoolMetrics = threadPoolBulkhead.getMetrics();
int queueDepth = threadPoolMetrics.getQueueDepth();
int numberOfRunningCalls = threadPoolMetrics.getNumberOfRunningCalls();
int numberOfThreadPoolThreads = threadPoolMetrics.getThreadPoolSize();

logger.info("Queue depth: {}, Running calls: {}, Thread pool size: {}",
    queueDepth, numberOfRunningCalls, numberOfThreadPoolThreads);

Time Limiter

Configuration de Base

1

Création d'un Time Limiter :

import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import java.time.Duration;
import java.util.concurrent.*;

// Configuration du Time Limiter
TimeLimiterConfig config = TimeLimiterConfig.custom()
    .timeoutDuration(Duration.ofSeconds(5))     // Timeout de 5 secondes
    .cancelRunningFuture(true)                  // Annuler le Future si timeout
    .build();

// Création du Time Limiter
TimeLimiter timeLimiter = TimeLimiter.of("backendService", config);

Explication des Propriétés

  • timeoutDuration(5s) :

    Durée maximale d'exécution avant timeout.

  • cancelRunningFuture(true) :

    Annule le Future en cours si timeout atteint.

2

Utilisation du Time Limiter :

// Méthode à limiter dans le temps (doit retourner CompletableFuture)
Supplier<CompletionStage<String>> supplier = () -> 
    CompletableFuture.supplyAsync(() -> externalService.call());

// Application du Time Limiter
CompletionStage<String> future = timeLimiter
    .executeCompletionStage(scheduledExecutorService, supplier);

// Exécution avec gestion du timeout
try {
    String result = future.toCompletableFuture().get(10, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    logger.warn("Call timed out after {} seconds", 10);
    return "Service timeout";
} catch (Exception e) {
    logger.error("Error calling backend service", e);
    return "Error occurred";
}

Configuration Avancée

1

Timeouts différents selon les services :

// Time limiter pour les services rapides
TimeLimiterConfig fastServiceConfig = TimeLimiterConfig.custom()
    .timeoutDuration(Duration.ofSeconds(2))
    .cancelRunningFuture(true)
    .build();

// Time limiter pour les services lents
TimeLimiterConfig slowServiceConfig = TimeLimiterConfig.custom()
    .timeoutDuration(Duration.ofSeconds(30))
    .cancelRunningFuture(true)
    .build();
2

Gestion fine de l'annulation :

TimeLimiterConfig config = TimeLimiterConfig.custom()
    .timeoutDuration(Duration.ofSeconds(10))
    .cancelRunningFuture(false) // Ne pas annuler immédiatement
    .build();

Événements et Monitoring

1

Écoute des événements :

// Écoute des événements du Time Limiter
timeLimiter.getEventPublisher()
    .onTimeout(event -> {
        logger.warn("Time limiter timeout for {}", event.getName());
    })
    .onError(event -> {
        logger.error("Time limiter error for {}: {}", 
            event.getName(), event.getThrowable().getMessage());
    });

Cache

Configuration de Base

1

Création d'un Cache :

import io.github.resilience4j.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import java.util.concurrent.TimeUnit;

// Configuration du cache JCache
CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();
MutableConfiguration<String, String> config = new MutableConfiguration<String, String>()
    .setTypes(String.class, String.class)
    .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_MINUTE))
    .setStatisticsEnabled(true);

javax.cache.Cache<String, String> jcache = cacheManager.createCache("backendService", config);

// Création du Cache Resilience4j
Cache<String, String> cache = Cache.of(jcache);
2

Utilisation du Cache :

// Méthode à mettre en cache
Function<String, String> function = (key) -> externalService.getData(key);

// Application du Cache
Function<String, String> decoratedFunction = Cache
    .decorateFunction(cache, function);

// Exécution avec cache
String result = decoratedFunction.apply("key1");

// Vérification si le résultat vient du cache
Cache.Metrics metrics = cache.getMetrics();
long cacheHits = metrics.getNumberOfCacheHits();
long cacheMisses = metrics.getNumberOfCacheMisses();

logger.info("Cache hits: {}, Cache misses: {}", cacheHits, cacheMisses);

Configuration Avancée

1

Cache avec expiration personnalisée :

MutableConfiguration<String, String> config = new MutableConfiguration<String, String>()
    .setTypes(String.class, String.class)
    .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.FIVE_MINUTES))
    .setStatisticsEnabled(true)
    .setStoreByValue(false); // Stockage par référence pour performance
2

Cache avec taille maximale :

// Utilisation de Caffeine comme backend de cache
MutableConfiguration<String, String> config = new MutableConfiguration<String, String>()
    .setTypes(String.class, String.class)
    .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.TEN_MINUTES))
    .setStatisticsEnabled(true);

// Configuration Caffeine (nécessite dépendance caffeine-jcache)
// Dans application.properties :
// caffeine.jcache.maximum-size=1000
// caffeine.jcache.record-stats=true

Configuration de Base

Installation et Dépendances

1

Ajout des dépendances Maven :

<!-- Core Resilience4j -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-all</artifactId>
    <version>2.1.0</version>
</dependency>

<!-- Spring Boot Integration -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.1.0</version>
</dependency>

<!-- Reactive support (si nécessaire) -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-reactor</artifactId>
    <version>2.1.0</version>
</dependency>

<!-- Cache support -->
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
2

Configuration Spring Boot :

// Classe principale
@SpringBootApplication
public class Resilience4jApplication {
    public static void main(String[] args) {
        SpringApplication.run(Resilience4jApplication.class, args);
    }
}

Configuration Globale

1

Configuration dans application.yml :

resilience4j:
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 50
        slowCallRateThreshold: 100
        slowCallDurationThreshold: 2s
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3
        minimumNumberOfCalls: 10
        slidingWindowType: TIME_BASED
        slidingWindowSize: 100
        recordExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
    instances:
      backendService:
        baseConfig: default
        failureRateThreshold: 30
        waitDurationInOpenState: 30s

  retry:
    configs:
      default:
        maxAttempts: 3
        waitDuration: 1s
        retryExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
    instances:
      backendService:
        baseConfig: default
        maxAttempts: 5

  ratelimiter:
    configs:
      default:
        limitRefreshPeriod: 1s
        limitForPeriod: 10
        timeoutDuration: 5s
    instances:
      backendService:
        baseConfig: default
        limitForPeriod: 50

  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 10
        maxWaitDuration: 5s
    instances:
      backendService:
        baseConfig: default
        maxConcurrentCalls: 20

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s
        cancelRunningFuture: true
    instances:
      backendService:
        baseConfig: default
        timeoutDuration: 10s

Annotations Resilience4j

@CircuitBreaker

1

Annotation de base :

@Service
public class BackendService {
    
    @CircuitBreaker(name = "backendService", fallbackMethod = "fallback")
    public String processRequest(String request) {
        // Logique métier qui peut échouer
        return externalService.call(request);
    }
    
    public String fallback(String request, Exception ex) {
        logger.warn("Fallback called for request: " + request, ex);
        return "Fallback response for: " + request;
    }
}
2

Configuration avancée :

@CircuitBreaker(
    name = "backendService",
    fallbackMethod = "fallback",
    ignoreExceptions = {ValidationException.class} // Ignorer certaines exceptions
)
public String processRequest(String request) {
    return externalService.call(request);
}

public String fallback(String request, ValidationException ex) {
    // Fallback spécifique pour ValidationException
    return "Invalid request: " + request;
}

public String fallback(String request, Exception ex) {
    // Fallback général
    return "Service temporarily unavailable";
}

@Retry

1

Annotation de base :

@Service
public class BackendService {
    
    @Retry(name = "backendService", fallbackMethod = "fallback")
    public String processRequest(String request) {
        return externalService.call(request);
    }
    
    public String fallback(String request, Exception ex) {
        logger.error("All retry attempts failed for request: " + request, ex);
        return "Default response";
    }
}
2

Configuration avec résultats à retenter :

@Retry(
    name = "backendService",
    fallbackMethod = "fallback",
    resultPredicate = Response::isEmpty // Retenter si le résultat est vide
)
public Response processRequest(String request) {
    return externalService.getResponse(request);
}

public Response fallback(String request, Exception ex) {
    return new Response("Fallback response");
}

@RateLimiter

1

Annotation de base :

@Service
public class BackendService {
    
    @RateLimiter(name = "backendService", fallbackMethod = "fallback")
    public String processRequest(String request) {
        return externalService.call(request);
    }
    
    public String fallback(String request, Exception ex) {
        if (ex instanceof RequestNotPermitted) {
            logger.warn("Rate limit exceeded for request: " + request);
            return "Rate limit exceeded";
        }
        return "Service temporarily unavailable";
    }
}

@Bulkhead

1

Semaphore Bulkhead :

@Service
public class BackendService {
    
    @Bulkhead(name = "backendService", fallbackMethod = "fallback")
    public String processRequest(String request) {
        return externalService.call(request);
    }
    
    public String fallback(String request, Exception ex) {
        if (ex instanceof BulkheadFullException) {
            logger.warn("Bulkhead full for request: " + request);
            return "Service temporarily unavailable due to high load";
        }
        return "Service temporarily unavailable";
    }
}
2

ThreadPool Bulkhead :

@Service
public class BackendService {
    
    @Bulkhead(name = "backendService", type = Bulkhead.Type.THREADPOOL, fallbackMethod = "fallback")
    public CompletableFuture<String> processRequestAsync(String request) {
        return CompletableFuture.supplyAsync(() -> externalService.call(request));
    }
    
    public CompletableFuture<String> fallback(String request, Exception ex) {
        if (ex instanceof BulkheadFullException) {
            logger.warn("ThreadPool bulkhead full for request: " + request);
            return CompletableFuture.completedFuture("Service temporarily unavailable due to high load");
        }
        return CompletableFuture.completedFuture("Service temporarily unavailable");
    }
}

Combinaison d'Annotations

1

Combinaison de plusieurs patterns :

@Service
public class BackendService {
    
    @CircuitBreaker(name = "backendService", fallbackMethod = "circuitBreakerFallback")
    @Retry(name = "backendService", fallbackMethod = "retryFallback")
    @RateLimiter(name = "backendService")
    @Bulkhead(name = "backendService")
    public String processRequest(String request) {
        return externalService.call(request);
    }
    
    public String circuitBreakerFallback(String request, Exception ex) {
        logger.warn("Circuit breaker fallback for request: " + request, ex);
        return "Circuit breaker fallback";
    }
    
    public String retryFallback(String request, Exception ex) {
        logger.error("Retry fallback for request: " + request, ex);
        return "Retry fallback";
    }
}
2

Ordre d'application :

// Ordre d'application des décorateurs (de l'intérieur vers l'extérieur) :
// 1. Bulkhead (le plus interne)
// 2. RateLimiter
// 3. Retry
// 4. CircuitBreaker (le plus externe)

// Ce qui équivaut à :
Supplier<String> decoratedSupplier = Decorators.ofSupplier(supplier)
    .withCircuitBreaker(circuitBreaker)
    .withRetry(retry)
    .withRateLimiter(rateLimiter)
    .withBulkhead(bulkhead)
    .decorate();

Configuration Avancée

Configuration Programmique

1

Configuration via Customizer :

@Configuration
public class Resilience4jConfig {
    
    @Bean
    public CircuitBreakerRegistryCustomizer<CircuitBreakerConfig, CircuitBreakerRegistry> 
    circuitBreakerCustomizer() {
        return CircuitBreakerRegistryCustomizer.of(
            CircuitBreakerConfig.custom()
                .failureRateThreshold(40)
                .waitDurationInOpenState(Duration.ofSeconds(20))
                .build(),
            "backendService"
        );
    }
    
    @Bean
    public RetryRegistryCustomizer<RetryConfig, RetryRegistry> 
    retryCustomizer() {
        return RetryRegistryCustomizer.of(
            RetryConfig.custom()
                .maxAttempts(5)
                .waitDuration(Duration.ofSeconds(2))
                .build(),
            "backendService"
        );
    }
}
2

Configuration dynamique :

@Service
public class Resilience4jDynamicConfig {
    
    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;
    
    @Autowired
    private RetryRegistry retryRegistry;
    
    public void updateCircuitBreakerConfig(String name, int failureRateThreshold) {
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(name);
        CircuitBreakerConfig newConfig = circuitBreaker.getCircuitBreakerConfig()
            .toBuilder()
            .failureRateThreshold(failureRateThreshold)
            .build();
        
        // Mettre à jour la configuration
        circuitBreaker.changeConfig(newConfig);
    }
    
    public void updateRetryConfig(String name, int maxAttempts) {
        Retry retry = retryRegistry.retry(name);
        RetryConfig newConfig = retry.getRetryConfig()
            .toBuilder()
            .maxAttempts(maxAttempts)
            .build();
        
        // Mettre à jour la configuration
        retry.changeConfig(newConfig);
    }
}

Configuration par Environnement

1

Configuration différente selon l'environnement :

# application.yml (configuration par défaut)
resilience4j:
  circuitbreaker:
    instances:
      backendService:
        failureRateThreshold: 50
        waitDurationInOpenState: 10s

---
# application-dev.yml
spring:
  profiles: dev

resilience4j:
  circuitbreaker:
    instances:
      backendService:
        failureRateThreshold: 90  # Plus tolérant en développement
        waitDurationInOpenState: 5s

---
# application-prod.yml
spring:
  profiles: prod

resilience4j:
  circuitbreaker:
    instances:
      backendService:
        failureRateThreshold: 30  # Plus strict en production
        waitDurationInOpenState: 30s
2

Configuration basée sur les propriétés :

@Configuration
public class Resilience4jPropertyConfig {
    
    @Value("${resilience4j.circuitbreaker.backendService.failureRateThreshold:50}")
    private int failureRateThreshold;
    
    @Value("${resilience4j.circuitbreaker.backendService.waitDurationInOpenState:10s}")
    private Duration waitDurationInOpenState;
    
    @Bean
    public CircuitBreaker circuitBreaker() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(failureRateThreshold)
            .waitDurationInOpenState(waitDurationInOpenState)
            .build();
        
        return CircuitBreaker.of("backendService", config);
    }
}

Configuration des Fallbacks

1

Fallbacks conditionnels :

@Service
public class BackendService {
    
    @CircuitBreaker(name = "backendService", fallbackMethod = "fallback")
    public String processRequest(String request) {
        return externalService.call(request);
    }
    
    public String fallback(String request, CallNotPermittedException ex) {
        // Fallback spécifique pour circuit breaker ouvert
        logger.warn("Circuit breaker open for request: " + request);
        return "Service temporarily unavailable - please try again later";
    }
    
    public String fallback(String request, BulkheadFullException ex) {
        // Fallback spécifique pour bulkhead plein
        logger.warn("Bulkhead full for request: " + request);
        return "Service busy - please try again in a moment";
    }
    
    public String fallback(String request, Exception ex) {
        // Fallback général pour toutes les autres exceptions
        logger.error("Error processing request: " + request, ex);
        return "Service temporarily unavailable";
    }
}
2

Fallbacks avec logique complexe :

@Service
public class BackendService {
    
    @Autowired
    private CacheService cacheService;
    
    @Autowired
    private AlertService alertService;
    
    @CircuitBreaker(name = "backendService", fallbackMethod = "fallback")
    public String processRequest(String request) {
        return externalService.call(request);
    }
    
    public String fallback(String request, Exception ex) {
        logger.warn("Fallback activated for request: " + request, ex);
        
        // Enregistrer l'alerte
        alertService.sendAlert("Fallback activated for request: " + request);
        
        // Essayer de récupérer du cache
        String cachedResult = cacheService.getFromCache(request);
        if (cachedResult != null) {
            logger.info("Returning cached result for request: " + request);
            return cachedResult + " (cached)";
        }
        
        // Retourner une réponse par défaut
        return "Service temporarily unavailable - using default response";
    }
}

Monitoring et Métriques

Configuration des Métriques

1

Activation des métriques :

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    metrics:
      enabled: true
    prometheus:
      enabled: true

resilience4j:
  metrics:
    enabled: true
    use-legacy-metric-format: false
2

Configuration des métriques détaillées :

resilience4j:
  metrics:
    enabled: true
    distribution:
      percentiles-histogram:
        circuitbreaker: true
        retry: true
        ratelimiter: true
        bulkhead: true
      percentiles:
        circuitbreaker:
          - 0.5
          - 0.9
          - 0.95
          - 0.99
      sla:
        circuitbreaker:
          - 100ms
          - 500ms
          - 1000ms
          - 5000ms

Métriques Disponibles

1

Métriques du Circuit Breaker :

# Métriques principales
resilience4j.circuitbreaker.state{kind="successful",name="backendService"}
resilience4j.circuitbreaker.state{kind="failed",name="backendService"}
resilience4j.circuitbreaker.state{kind="not_permitted",name="backendService"}
resilience4j.circuitbreaker.state{state="closed",name="backendService"}
resilience4j.circuitbreaker.state{state="open",name="backendService"}
resilience4j.circuitbreaker.state{state="half_open",name="backendService"}

# Métriques de latence
resilience4j.circuitbreaker.calls{kind="successful",name="backendService"}
resilience4j.circuitbreaker.calls{kind="failed",name="backendService"}
resilience4j.circuitbreaker.calls{kind="not_permitted",name="backendService"}
resilience4j.circuitbreaker.calls{kind="slow_successful",name="backendService"}
resilience4j.circuitbreaker.calls{kind="slow_failed",name="backendService"}

# Histogrammes de latence
resilience4j.circuitbreaker.call_duration_seconds{name="backendService"}
2

Métriques du Retry :

# Métriques de retry
resilience4j.retry.calls{kind="successful_without_retry",name="backendService"}
resilience4j.retry.calls{kind="successful_with_retry",name="backendService"}
resilience4j.retry.calls{kind="failed_without_retry",name="backendService"}
resilience4j.retry.calls{kind="failed_with_retry",name="backendService"}

# Histogrammes de tentatives
resilience4j.retry.retry_calls{name="backendService"}
3

Métriques du Rate Limiter :

# Métriques de rate limiting
resilience4j.ratelimiter.available_permissions{name="backendService"}
resilience4j.ratelimiter.waiting_threads{name="backendService"}
resilience4j.ratelimiter.successful_calls{name="backendService"}
resilience4j.ratelimiter.failed_calls{name="backendService"}

Intégration avec Prometheus et Grafana

1

Configuration Prometheus :

# prometheus.yml
scrape_configs:
  - job_name: 'spring-boot-app'
    static_configs:
      - targets: ['localhost:8080']
    metrics_path: '/actuator/prometheus'
    scrape_interval: 15s
2

Dashboard Grafana pour Circuit Breaker :

# Panel 1: État du Circuit Breaker
resilience4j_circuitbreaker_state{name="backendService"}

# Panel 2: Taux d'erreurs
rate(resilience4j_circuitbreaker_calls{kind="failed"}[5m]) / 
rate(resilience4j_circuitbreaker_calls[5m]) * 100

# Panel 3: Latence 95th percentile
histogram_quantile(0.95, sum(rate(resilience4j_circuitbreaker_call_duration_seconds_bucket[5m])) by (le))

# Panel 4: Nombre d'appels
sum(rate(resilience4j_circuitbreaker_calls[5m])) by (kind)
3

Alertes Prometheus :

# Alerte pour taux d'erreurs élevé
groups:
  - name: resilience4j-alerts
    rules:
      - alert: HighFailureRate
        expr: rate(resilience4j_circuitbreaker_calls{kind="failed"}[5m]) / 
              rate(resilience4j_circuitbreaker_calls[5m]) > 0.5
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "High failure rate detected"
          description: "Failure rate above 50% for circuit breaker {{ $labels.name }}"

      - alert: CircuitBreakerOpen
        expr: resilience4j_circuitbreaker_state{state="open"} == 1
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "Circuit breaker is open"
          description: "Circuit breaker {{ $labels.name }} is open"

Événements et Logging

1

Configuration du logging :

# application.yml
logging:
  level:
    io.github.resilience4j: DEBUG
    io.github.resilience4j.circuitbreaker: INFO
    io.github.resilience4j.retry: INFO
    io.github.resilience4j.ratelimiter: INFO
    io.github.resilience4j.bulkhead: INFO
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
2

Écoute des événements personnalisés :

@Component
public class Resilience4jEventListener {
    
    private static final Logger logger = LoggerFactory.getLogger(Resilience4jEventListener.class);
    
    @EventListener
    public void onCircuitBreakerEvent(CircuitBreakerOnFailureEvent event) {
        logger.warn("Circuit breaker failure: {} - {}", 
            event.getCircuitBreakerName(), event.getThrowable().getMessage());
    }
    
    @EventListener
    public void onCircuitBreakerEvent(CircuitBreakerOnSuccessEvent event) {
        logger.info("Circuit breaker success: {}", event.getCircuitBreakerName());
    }
    
    @EventListener
    public void onCircuitBreakerEvent(CircuitBreakerOnStateTransitionEvent event) {
        logger.info("Circuit breaker state transition: {} - {} -> {}", 
            event.getCircuitBreakerName(), 
            event.getStateTransition().getFromState(),
            event.getStateTransition().getToState());
    }
    
    @EventListener
    public void onRetryEvent(RetryOnRetryEvent event) {
        logger.warn("Retry attempt {}: {} - {}", 
            event.getNumberOfRetryAttempts(), 
            event.getName(), 
            event.getLastThrowable().getMessage());
    }
}

Intégration avec Spring Boot

Configuration Auto-configurée

1

Configuration minimale :

# application.yml
resilience4j:
  circuitbreaker:
    instances:
      backendService:
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3
        slidingWindowSize: 100

  retry:
    instances:
      backendService:
        maxAttempts: 3
        waitDuration: 1s

spring:
  application:
    name: resilience4j-demo
2

Service avec annotations :

@Service
public class BackendServiceClient {
    
    private final WebClient webClient;
    
    public BackendServiceClient(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.build();
    }
    
    @CircuitBreaker(name = "backendService", fallbackMethod = "fallback")
    @Retry(name = "backendService")
    public Mono<String> callBackend(String endpoint) {
        return webClient.get()
            .uri(endpoint)
            .retrieve()
            .bodyToMono(String.class);
    }
    
    public Mono<String> fallback(String endpoint, Exception ex) {
        logger.warn("Fallback for endpoint: " + endpoint, ex);
        return Mono.just("Service temporarily unavailable");
    }
}

Intégration avec WebClient

1

Configuration WebClient :

@Configuration
public class WebClientConfig {
    
    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        return builder
            .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
            .build();
    }
}
2

Service avec WebClient et Resilience4j :

@Service
public class ExternalServiceClient {
    
    private final WebClient webClient;
    private final CircuitBreaker circuitBreaker;
    private final Retry retry;
    
    public ExternalServiceClient(
            WebClient webClient,
            CircuitBreakerRegistry circuitBreakerRegistry,
            RetryRegistry retryRegistry) {
        this.webClient = webClient;
        this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("externalService");
        this.retry = retryRegistry.retry("externalService");
    }
    
    public Mono<String> getData(String url) {
        Supplier<Mono<String>> supplier = () -> webClient.get()
            .uri(url)
            .retrieve()
            .bodyToMono(String.class);
        
        // Appliquer les patterns de résilience
        Supplier<Mono<String>> decoratedSupplier = Decorators.ofSupplier(supplier)
            .withCircuitBreaker(circuitBreaker)
            .withRetry(retry)
            .decorate();
        
        return decoratedSupplier.get()
            .onErrorResume(throwable -> {
                logger.error("Error calling external service: " + url, throwable);
                return Mono.just("Fallback data");
            });
    }
}

Intégration avec OpenFeign

1

Configuration Feign :

@FeignClient(
    name = "external-service",
    url = "${external.service.url:http://localhost:8081}",
    configuration = ExternalServiceFeignConfig.class
)
public interface ExternalServiceClient {
    
    @GetMapping("/data/{id}")
    String getData(@PathVariable("id") String id);
    
    @PostMapping("/process")
    String processData(@RequestBody String data);
}
2

Configuration Resilience4j pour Feign :

@Configuration
public class ExternalServiceFeignConfig {
    
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.BASIC;
    }
    
    @Bean
    public Request.Options options() {
        return new Request.Options(5000, 10000); // connectTimeout, readTimeout
    }
}

@Service
public class ExternalServiceWrapper {
    
    private final ExternalServiceClient externalServiceClient;
    private final CircuitBreaker circuitBreaker;
    private final Retry retry;
    
    public ExternalServiceWrapper(
            ExternalServiceClient externalServiceClient,
            CircuitBreakerRegistry circuitBreakerRegistry,
            RetryRegistry retryRegistry) {
        this.externalServiceClient = externalServiceClient;
        this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("externalService");
        this.retry = retryRegistry.retry("externalService");
    }
    
    @CircuitBreaker(name = "externalService", fallbackMethod = "fallback")
    @Retry(name = "externalService")
    public String getData(String id) {
        return externalServiceClient.getData(id);
    }
    
    public String fallback(String id, Exception ex) {
        logger.warn("Fallback for data request: " + id, ex);
        return "Default data";
    }
}

Gestion des Transactions

1

Transactions avec Resilience4j :

@Service
@Transactional
public class BusinessService {
    
    @Autowired
    private ExternalServiceClient externalServiceClient;
    
    @CircuitBreaker(name = "businessService", fallbackMethod = "rollbackTransaction")
    public void processBusinessLogic(String data) {
        // Logique métier qui peut échouer
        String result = externalServiceClient.processData(data);
        
        // Mise à jour de la base de données
        updateDatabase(result);
    }
    
    public void rollbackTransaction(String data, Exception ex) {
        logger.error("Rolling back transaction for data: " + data, ex);
        
        // Logique de rollback personnalisée
        // Note: @Transactional gère le rollback automatique pour les RuntimeException
        throw new BusinessException("Business operation failed", ex);
    }
    
    private void updateDatabase(String result) {
        // Mise à jour de la base de données
    }
}
2

Gestion des erreurs distribuées :

@Service
public class DistributedTransactionService {
    
    @Autowired
    private ServiceAClient serviceAClient;
    
    @Autowired
    private ServiceBClient serviceBClient;
    
    @CircuitBreaker(name = "distributedTransaction", fallbackMethod = "compensate")
    public void distributedOperation(String data) {
        // Opération sur Service A
        String resultA = serviceAClient.process(data);
        
        try {
            // Opération sur Service B
            String resultB = serviceBClient.process(resultA);
            
            // Confirmation de l'opération A
            serviceAClient.confirm(resultA);
        } catch (Exception e) {
            // Compensation de l'opération A
            serviceAClient.compensate(resultA);
            throw e;
        }
    }
    
    public void compensate(String data, Exception ex) {
        logger.error("Compensating distributed transaction for data: " + data, ex);
        // Logique de compensation
    }
}

Patterns de Résilience

Pattern du Fallback

Stratégies de Fallback

Le fallback est la réponse alternative fournie lorsque le service principal échoue.

1

Fallback statique :

@CircuitBreaker(name = "userService", fallbackMethod = "getDefaultUser")
public User getUser(Long userId) {
    return userServiceClient.getUser(userId);
}

public User getDefaultUser(Long userId, Exception ex) {
    logger.warn("Returning default user for ID: " + userId, ex);
    return new User(userId, "Default User", "default@example.com");
}
2

Fallback dynamique :

@CircuitBreaker(name = "userService", fallbackMethod = "getCachedUser")
public User getUser(Long userId) {
    return userServiceClient.getUser(userId);
}

public User getCachedUser(Long userId, Exception ex) {
    logger.warn("Service unavailable, checking cache for user ID: " + userId, ex);
    
    User cachedUser = cacheService.getUser(userId);
    if (cachedUser != null) {
        return cachedUser;
    }
    
    return new User(userId, "Cached Default User", "cached@example.com");
}
3

Fallback avec logique complexe :

@CircuitBreaker(name = "userService", fallbackMethod = "getDegradedUser")
public User getUserWithProfile(Long userId) {
    User user = userServiceClient.getUser(userId);
    UserProfile profile = profileServiceClient.getProfile(userId);
    user.setProfile(profile);
    return user;
}

public User getDegradedUser(Long userId, Exception ex) {
    logger.warn("Service degraded for user ID: " + userId, ex);
    
    // Service dégradé - données partielles
    User user = new User(userId, "Degraded User", "degraded@example.com");
    
    // Profil minimal
    UserProfile profile = new UserProfile();
    profile.setStatus("Service degraded");
    user.setProfile(profile);
    
    // Notification de l'erreur
    alertService.sendAlert("Fallback activated for user: " + userId);
    
    return user;
}

Pattern du Cache

Cache Pattern

Le cache pattern stocke les résultats pour améliorer les performances et la résilience.

1

Cache avec Resilience4j :

@Service
public class CachedUserService {
    
    private final Cache<String, User> userCache;
    private final UserServiceClient userServiceClient;
    
    public CachedUserService(UserServiceClient userServiceClient) {
        this.userServiceClient = userServiceClient;
        
        // Configuration du cache
        javax.cache.Cache<String, User> jcache = Caching.getCachingProvider()
            .getCacheManager()
            .createCache("users", new MutableConfiguration<String, User>()
                .setTypes(String.class, User.class)
                .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.FIVE_MINUTES))
                .setStatisticsEnabled(true));
        
        this.userCache = Cache.of(jcache);
    }
    
    @Cacheable(name = "users")
    public User getUser(Long userId) {
        return userServiceClient.getUser(userId);
    }
    
    public void evictUser(Long userId) {
        userCache.remove(String.valueOf(userId));
    }
}
2

Cache avec fallback :

@CircuitBreaker(name = "userService", fallbackMethod = "getCachedUser")
@Cacheable(name = "users")
public User getUser(Long userId) {
    return userServiceClient.getUser(userId);
}

public User getCachedUser(Long userId, Exception ex) {
    logger.warn("Service unavailable, returning cached user for ID: " + userId, ex);
    
    // Essayer de récupérer du cache
    User cachedUser = userCache.get(String.valueOf(userId));
    if (cachedUser != null) {
        return cachedUser;
    }
    
    // Retourner un utilisateur par défaut
    return new User(userId, "Cached Default User", "cached@example.com");
}

Pattern du Bulkhead

Isolation des Services

Le pattern Bulkhead isole les services pour éviter que la défaillance d'un service n'affecte les autres.

1

Isolation par groupes :

// Configuration pour les services critiques
@Configuration
public class CriticalServiceBulkheadConfig {
    
    @Bean
    public Bulkhead criticalServiceBulkhead() {
        return Bulkhead.of("criticalService", BulkheadConfig.custom()
            .maxConcurrentCalls(50)
            .maxWaitDuration(Duration.ofMillis(100))
            .build());
    }
}

// Configuration pour les services non critiques
@Configuration
public class NonCriticalServiceBulkheadConfig {
    
    @Bean
    public Bulkhead nonCriticalServiceBulkhead() {
        return Bulkhead.of("nonCriticalService", BulkheadConfig.custom()
            .maxConcurrentCalls(5)
            .maxWaitDuration(Duration.ofSeconds(1))
            .build());
    }
}
2

Limitation des ressources :

@Service
public class ResourceLimitedService {
    
    @Bulkhead(name = "criticalService")
    public CriticalData getCriticalData(String id) {
        return criticalServiceClient.getData(id);
    }
    
    @Bulkhead(name = "nonCriticalService")
    public NonCriticalData getNonCriticalData(String id) {
        return nonCriticalServiceClient.getData(id);
    }
}

Pattern du Timeout

Gestion des Timeouts

Les timeouts protègent contre les services lents qui pourraient bloquer l'ensemble du système.

1

Configuration des timeouts :

@TimeLimiter(name = "expensiveOperation", fallbackMethod = "getTimeoutFallback")
public CompletableFuture<ExpensiveOperation> performExpensiveOperation(Data data) {
    return CompletableFuture.supplyAsync(() -> expensiveServiceClient.process(data));
}

public CompletableFuture<ExpensiveOperation> getTimeoutFallback(Data data, Exception ex) {
    logger.warn("Timeout on expensive operation", ex);
    return CompletableFuture.completedFuture(
        new ExpensiveOperation("Operation cancelled - timeout"));
}
2

Timeouts adaptatifs :

@Service
public class AdaptiveTimeoutService {
    
    @TimeLimiter(name = "adaptiveService", fallbackMethod = "getAdaptiveFallback")
    public CompletableFuture<ServiceResponse> callService(ServiceRequest request) {
        int timeout = calculateTimeout(request);
        return CompletableFuture.supplyAsync(() -> serviceClient.call(request));
    }
    
    private int calculateTimeout(ServiceRequest request) {
        // Logique adaptative de calcul du timeout
        if (request.isComplex()) {
            return 10000; // 10 secondes pour les requêtes complexes
        }
        return 3000; // 3 secondes pour les requêtes simples
    }
    
    public CompletableFuture<ServiceResponse> getAdaptiveFallback(ServiceRequest request, Exception ex) {
        return CompletableFuture.completedFuture(
            new ServiceResponse("Service temporarily unavailable"));
    }
}

Bonnes Pratiques Resilience4j

Configuration et Déploiement

Principes Fondamentaux

  • Configuration externalisée : Utilisez des fichiers de configuration ou Config Server
  • Environnements séparés : Différentes configurations pour dev, test, prod
  • Versioning : Versionnez vos configurations avec le code
  • Documentation : Documentez clairement les stratégies de résilience

Configuration Recommandée

# Production
resilience4j:
  circuitbreaker:
    instances:
      default:
        failureRateThreshold: 50
        waitDurationInOpenState: 60s
        permittedNumberOfCallsInHalfOpenState: 10
  retry:
    instances:
      default:
        maxAttempts: 3
        waitDuration: 1s
  ratelimiter:
    instances:
      default:
        limitForPeriod: 10
        limitRefreshPeriod: 1s
        timeoutDuration: 5s
                    

Performance et Optimisation

Optimisations Clés

  • Configuration appropriée : Ajustez les seuils selon vos besoins spécifiques
  • Timeouts raisonnables : Ni trop courts (erreurs prématurées) ni trop longs (ressources bloquées)
  • Retry stratégique : Retry seulement sur les erreurs récupérables
  • Monitoring actif : Surveillez les métriques pour ajuster les configurations

Paramètres de Performance Optimisés

Paramètre Valeur Recommandée Raison
Circuit Breaker - failureRateThreshold 30-50% Équilibre entre réactivité et stabilité
Retry - maxAttempts 2-3 tentatives Évite la surcharge sans abandon prématuré
Rate Limiter - limitForPeriod Selon la capacité du service Protection contre les surcharges
Bulkhead - maxConcurrentCalls Selon les ressources disponibles Isolation des ressources

Sécurité et Résilience

Considérations de Sécurité

  • Validation des entrées : Validez toujours les données avant de les traiter
  • Chiffrement des communications : Utilisez HTTPS entre les services
  • Rate limiting : Protégez contre les abus et DDoS
  • Monitoring des accès : Surveillez les patterns d'accès inhabituels

Pièges Courants à Éviter

  • Timeouts trop courts : Causent des erreurs prématurées
  • Trop de retries : Amplifient les problèmes de performance
  • Pas de health checking : Envoient des requêtes à des services défaillants
  • Configuration statique : Difficile d'adapter aux conditions changeantes
  • Pas de monitoring : Difficile de diagnostiquer les problèmes

Tests et Validation

Stratégies de Test

  • Tests unitaires : Testez les configurations de Resilience4j
  • Tests d'intégration : Vérifiez l'interaction entre les patterns
  • Tests de charge : Simulez des scénarios de haute charge
  • Tests de chaos : Simulez des pannes de services
  • Tests de performance : Mesurez l'impact sur la latence
// Exemple de test de Circuit Breaker
@SpringBootTest
class CircuitBreakerTest {
    
    @Autowired
    private BackendService backendService;
    
    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;
    
    @Test
    void testCircuitBreakerOpens() {
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("backendService");
        
        // Simuler des échecs
        for (int i = 0; i < 10; i++) {
            try {
                backendService.processRequest("test");
            } catch (Exception e) {
                // Attendu
            }
        }
        
        // Vérifier que le circuit breaker est ouvert
        assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
    }
    
    @Test
    void testFallback() {
        // Simuler un service indisponible
        String result = backendService.processRequest("test");
        
        // Vérifier que le fallback est appelé
        assertThat(result).isEqualTo("Fallback response");
    }
}

Résumé des Bonnes Pratiques

En suivant ces bonnes pratiques, vous obtiendrez :

  • Haute disponibilité : Résilience face aux pannes
  • Bonne performance : Distribution optimale de la charge
  • Facilité de maintenance : Configuration claire et documentée
  • Sécurité : Communications sécurisées et contrôlées
  • Observabilité : Monitoring et alerting efficaces