Guide to Resilience4j

1. Vue d’ensemble

Dans ce didacticiel, nous allons parler de la bibliothèque Resilience4j .

  • La bibliothèque aide à mettre en œuvre des systèmes résilients en gérant la tolérance aux pannes pour les communications à distance. **

La bibliothèque est inspirée de Hystrix mais propose une API beaucoup plus pratique et un certain nombre d’autres fonctionnalités telles que Rate Limiter (blocage des requêtes trop fréquentes) beaucoup de demandes simultanées) etc.

2. Configuration Maven

Pour commencer, nous devons ajouter les modules cibles à notre pom.xml (ici, nous ajoutons le disjoncteur) :

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
    <version>0.12.1</version>
</dependency>

Ici, nous utilisons le module _circuitbreaker _ . Tous les modules et leurs dernières versions sont disponibles sur Maven Central .

Dans les sections suivantes, nous allons passer en revue les modules les plus couramment utilisés de la bibliothèque.

3. Disjoncteur

Notez que pour ce module, nous avons besoin de la dépendance resilience4j-circuitbreaker ci-dessus.

Le modèle Circuit Breaker nous aide à empêcher une cascade d’échecs lorsqu’un service distant est en panne.

  • Après un certain nombre de tentatives infructueuses, nous pouvons considérer que le service est indisponible/surchargé et rejeter avec empressement toutes les requêtes suivantes ** qui lui sont adressées. De cette manière, nous pouvons économiser les ressources système pour les appels susceptibles d’échouer.

Voyons comment nous pouvons y arriver avec Resilience4j.

Tout d’abord, nous devons définir les paramètres à utiliser. Le moyen le plus simple consiste à utiliser les paramètres par défaut:

CircuitBreakerRegistry circuitBreakerRegistry
  = CircuitBreakerRegistry.ofDefaults();

Il est également possible d’utiliser des paramètres personnalisés:

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
  .failureRateThreshold(20)
  .ringBufferSizeInClosedState(5)
  .build();

Nous avons défini ici le seuil de taux à 20% et un nombre minimum de 5 tentatives d’appel.

Ensuite, nous créons un objet CircuitBreaker et appelons le service distant via celui-ci:

interface RemoteService {
    int process(int i);
}

CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = registry.circuitBreaker("my");
Function<Integer, Integer> decorated = CircuitBreaker
  .decorateFunction(circuitBreaker, service::process);

Enfin, voyons comment cela fonctionne à travers un test JUnit.

Nous essaierons d’appeler le service 10 fois. Nous devrions pouvoir vérifier que l’appel a été tenté 5 fois au minimum, puis arrêté dès que 20% des appels ont échoué:

when(service.process(any(Integer.class))).thenThrow(new RuntimeException());

for (int i = 0; i < 10; i++) {
    try {
        decorated.apply(i);
    } catch (Exception ignore) {}
}

verify(service, times(5)).process(any(Integer.class));

3.1. Les états et réglages du disjoncteur **

Un CircuitBreaker peut être dans l’un des trois états suivants:

  • CLOSED - tout va bien, pas de court-circuit impliqué

  • OPEN - le serveur distant est en panne, toutes les demandes qui lui sont adressées sont court-circuitées

  • HALF OPEN__ - durée configurée depuis le passage à l’état OPEN

s’est écoulé et CircuitBreaker permet aux demandes de vérifier si le service distant est de nouveau en ligne

Nous pouvons configurer les paramètres suivants:

  • le seuil de taux d’échec au-dessus duquel le CircuitBreaker s’ouvre et

commence à court-circuiter des appels ** la durée d’attente qui définit combien de temps le CircuitBreaker doit

rester ouvert avant de passer à la moitié ouverte ** la taille de l’anneau tampon lorsque le CircuitBreaker est à moitié ouvert ou

fermé ** un CircuitBreakerEventListener personnalisé qui gère CircuitBreaker

événements ** un Predicate personnalisé qui évalue si une exception doit compter comme un

échec et donc augmenter le taux d’échec

4. Limiteur de taux

Semblable à la section précédente, cette fonctionnalité nécessite la dépendance https://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%3A%22resilience4j-ratelimiter%22&silience4j-ratelimiter__ ].

Comme son nom l’indique, cette fonctionnalité permet de limiter l’accès à certains services . Son API est très similaire à CircuitBreaker’s - il existe des classes Registry , Config et Limiter .

Voici un exemple de son apparence:

RateLimiterConfig config = RateLimiterConfig.custom().limitForPeriod(2).build();
RateLimiterRegistry registry = RateLimiterRegistry.of(config);
RateLimiter rateLimiter = registry.rateLimiter("my");
Function<Integer, Integer> decorated
  = RateLimiter.decorateFunction(rateLimiter, service::process);

Désormais, tous les appels sur le bloc de service décoré sont nécessaires pour se conformer à la configuration du limiteur de débit.

Nous pouvons configurer des paramètres tels que:

  • la période de l’actualisation de limite

  • la limite d’autorisations pour la période d’actualisation

  • l’attente par défaut de la durée de l’autorisation

5. Cloison

Ici, nous avons d’abord besoin de la dépendance resilience4j-bulkhead .

Il est possible de limiter le nombre d’appels simultanés à un service particulier.

Voyons un exemple d’utilisation de l’API Bulkhead pour configurer un nombre maximal d’appels simultanés:

BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(1).build();
BulkheadRegistry registry = BulkheadRegistry.of(config);
Bulkhead bulkhead = registry.bulkhead("my");
Function<Integer, Integer> decorated
  = Bulkhead.decorateFunction(bulkhead, service::process);

Pour tester cette configuration, nous appellerons la méthode d’un service factice.

Ensuite, nous nous assurons que Bulkhead n’autorise aucun autre appel:

CountDownLatch latch = new CountDownLatch(1);
when(service.process(anyInt())).thenAnswer(invocation -> {
    latch.countDown();
    Thread.currentThread().join();
    return null;
});

ForkJoinTask<?> task = ForkJoinPool.commonPool().submit(() -> {
    try {
        decorated.apply(1);
    } finally {
        bulkhead.onComplete();
    }
});
latch.await();
assertThat(bulkhead.isCallPermitted()).isFalse();

Nous pouvons configurer les paramètres suivants:

  • le nombre maximal d’exécutions parallèles autorisées par la cloison

  • le temps maximum qu’un thread attendra pour tenter d’entrer

une cloison saturée

6. Réessayez

Pour cette fonctionnalité, nous devrons ajouter la bibliothèque resilience4j-retry au projet.

Nous pouvons réessayer automatiquement un appel ayant échoué à l’aide de l’API Réessayer:

RetryConfig config = RetryConfig.custom().maxAttempts(2).build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("my");
Function<Integer, Void> decorated
  = Retry.decorateFunction(retry, (Integer s) -> {
        service.process(s);
        return null;
    });

Imaginons maintenant une situation dans laquelle une exception est levée lors d’un appel de service distant et assurons-nous que la bibliothèque relance automatiquement l’appel ayant échoué:

when(service.process(anyInt())).thenThrow(new RuntimeException());
try {
    decorated.apply(1);
    fail("Expected an exception to be thrown if all retries failed");
} catch (Exception e) {
    verify(service, times(2)).process(any(Integer.class));
}

Nous pouvons également configurer les éléments suivants:

  • le nombre de tentatives maximum

  • la durée d’attente avant les tentatives

  • une fonction personnalisée pour modifier l’intervalle d’attente après un échec

  • un Predicate personnalisé qui évalue si une exception doit avoir pour résultat

réessayer l’appel

7. Cache

Le module Cache nécessite la dépendance resilience4j-cache .

L’initialisation est légèrement différente des autres modules:

javax.cache.Cache cache = ...;//Use appropriate cache here
Cache<Integer, Integer> cacheContext = Cache.of(cache);
Function<Integer, Integer> decorated
  = Cache.decorateSupplier(cacheContext, () -> service.process(1));

Ici, la mise en cache est effectuée par la mise en œuvre JSR-107 et Resilience4j permet de l’appliquer.

Notez qu’il n’y a pas d’API pour décorer les fonctions (comme Cache.decorateFunction (Fonction) ), l’API ne prend en charge que les types Supplier et Callable .

8. TimeLimiter

Pour ce module, nous devons ajouter la dépendance resilience4j-timelimiter .

Il est possible de limiter le temps passé à appeler un service distant à l’aide de TimeLimiter.

Pour illustrer cela, configurons un TimeLimiter avec un délai d’attente configuré de 1 milliseconde:

long ttl = 1;
TimeLimiterConfig config
  = TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(ttl)).build();
TimeLimiter timeLimiter = TimeLimiter.of(config);

Ensuite, vérifions que Resilience4j appelle Future.get () avec le délai d’expiration prévu:

Future futureMock = mock(Future.class);
Callable restrictedCall
  = TimeLimiter.decorateFutureSupplier(timeLimiter, () -> futureMock);
restrictedCall.call();

verify(futureMock).get(ttl, TimeUnit.MILLISECONDS);

Nous pouvons également le combiner avec CircuitBreaker :

Callable chainedCallable
  = CircuitBreaker.decorateCallable(circuitBreaker, restrictedCall);

9. Modules complémentaires

Resilience4j propose également un certain nombre de modules complémentaires facilitant son intégration aux frameworks et bibliothèques populaires.

Certaines des intégrations les plus connues sont:

  • Botte de printemps - module resilience4j-spring-boot

  • Ratpack - module resilience4j-ratpack

  • Retrofit - module resilience4j-retrofit

  • Vertx - module resilience4j-vertx

  • Dropwizard - module resilience4j-metrics

  • Prométhée - module __resilience4j-prométhée

10. Conclusion

Dans cet article, nous avons étudié différents aspects de la bibliothèque Resilience4j et avons appris à l’utiliser pour résoudre divers problèmes de tolérance aux pannes dans les communications entre serveurs.

Comme toujours, le code source des exemples ci-dessus peut être trouvé à over sur GitHub .