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.
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");
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
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).
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
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();
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();
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
É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"));
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
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.
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
Attente fixe :
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofSeconds(2)) // 2 secondes fixes entre chaque tentative
.build();
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
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%
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
É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());
});
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
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.
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
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();
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();
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
É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());
});
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é
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);
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
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";
}
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
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();
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
É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());
});
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
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.
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
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();
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
É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
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);
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
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
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
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>
Configuration Spring Boot :
// Classe principale
@SpringBootApplication
public class Resilience4jApplication {
public static void main(String[] args) {
SpringApplication.run(Resilience4jApplication.class, args);
}
}
Configuration Globale
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
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;
}
}
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
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";
}
}
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
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
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";
}
}
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
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";
}
}
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
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"
);
}
}
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
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
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
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";
}
}
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
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
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
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"}
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"}
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
Configuration Prometheus :
# prometheus.yml
scrape_configs:
- job_name: 'spring-boot-app'
static_configs:
- targets: ['localhost:8080']
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
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)
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
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"
É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
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
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
Configuration WebClient :
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
}
}
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
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);
}
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
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
}
}
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.
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");
}
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");
}
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.
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));
}
}
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.
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());
}
}
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.
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"));
}
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