Cette page rassemble les motifs concrets rencontrés en production : un client REST robuste qui combine retry + timeout + circuit breaker + fallback, l’usage de FallbackHandler<T> pour des fallbacks réutilisables, la sémantique d'`@Asynchronous` avec virtual threads, et les surcharges MP Config sans recompilation.
Client REST robuste
La combinaison standard pour appeler un service distant via Cassini (ou tout client HTTP) : timeout par tentative, retry borné, circuit breaker, fallback.
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.faulttolerance.*;
@ApplicationScoped
public class CatalogClient {
@Inject
private CatalogRemote remote;
@Timeout(value = 2000) // 2 s par tentative
@Retry(maxRetries = 3, delay = 200, jitter = 100,
retryOn = { java.io.IOException.class, java.util.concurrent.TimeoutException.class },
abortOn = { IllegalArgumentException.class })
@CircuitBreaker(requestVolumeThreshold = 20, failureRatio = 0.5,
delay = 5000, successThreshold = 3)
@Fallback(fallbackMethod = "catalogFromCache")
public Catalog fetch(String tenant) {
return remote.fetch(tenant);
}
private Catalog catalogFromCache(String tenant) {
return Catalog.emptyFor(tenant);
}
}
Sémantique :
-
Chaque tentative est bornée à 2 s par
@Timeout. -
@Retryréessaie 3 fois, avec un délai de base de 200 ms et un jitter de ±100 ms — l’évite le thundering herd. -
retryOnrestreint le retry àIOExceptionetTimeoutException.abortOnarrête tout retry surIllegalArgumentException(erreur métier, ne reviendra pas). -
@CircuitBreakerobserve les 20 derniers résultats ; au-dessus de 50 % d’échecs, il s’ouvre pour 5 s. -
@Fallback(fallbackMethod = "catalogFromCache")fournit le catalogue vide quand toutes les tentatives ont échoué ou que le circuit est ouvert.
FallbackHandler<T> typé et réutilisable
Quand le même fallback s’applique à plusieurs méthodes, un FallbackHandler<T> séparé est plus propre qu’une méthode privée par bean.
import org.eclipse.microprofile.faulttolerance.ExecutionContext;
import org.eclipse.microprofile.faulttolerance.FallbackHandler;
public class CatalogCacheFallback implements FallbackHandler<Catalog> {
@Override
public Catalog handle(ExecutionContext ctx) {
String tenant = (String) ctx.getParameters()[0];
// ctx.getFailure() expose l'exception qui a déclenché le fallback
return Catalog.emptyFor(tenant);
}
}
@ApplicationScoped
public class CatalogClient {
@Fallback(CatalogCacheFallback.class)
@Retry(maxRetries = 3)
public Catalog fetch(String tenant) {
return remote.fetch(tenant);
}
}
Le FallbackHandler est instancié par CDI — il peut donc injecter ses propres dépendances (@Inject CacheManager cache;).
Bulkhead synchrone : limiter la concurrence
@Bulkhead(N) borne le nombre d’invocations simultanées. Pratique pour protéger un pool de connexions sous-dimensionné.
@ApplicationScoped
public class DbReader {
@Bulkhead(10) // au plus 10 lectures simultanées
@Timeout(500)
public Row readRow(long id) {
return dataSource.query("SELECT * FROM t WHERE id = ?", id);
}
}
Au-delà de 10 appels concurrents, les suivants reçoivent un BulkheadException immédiat — pas de file d’attente en mode synchrone.
@Asynchronous + @Bulkhead : file d’attente bornée
En combinant les deux, on obtient une file d’attente bornée. value borne la concurrence, waitingTaskQueue borne la file.
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CompletableFuture;
@ApplicationScoped
public class IndexingService {
@Asynchronous
@Bulkhead(value = 5, waitingTaskQueue = 100)
@Retry(maxRetries = 2)
public CompletionStage<Indexed> index(Document doc) {
Indexed result = expensiveIndexing(doc);
return CompletableFuture.completedStage(result);
}
}
L’appelant récupère un CompletionStage<Indexed> immédiatement ; le calcul s’exécute sur un virtual thread créé par l’intercepteur. Au-delà de 5 calculs concurrents + 100 en file, le CompletionStage retourné se complète en erreur avec BulkheadException.
@Asynchronous seul : déléguer à un virtual thread
Pour une opération longue qu’on ne veut pas bloquer le thread appelant :
@ApplicationScoped
public class EmailService {
@Asynchronous
@Timeout(5000)
@Retry(maxRetries = 3, delay = 1000)
public CompletionStage<Receipt> send(Email mail) {
Receipt r = smtpClient.send(mail);
return CompletableFuture.completedStage(r);
}
}
Le @Timeout borne la tentative, pas la séquence. Trois tentatives × 5 s = jusqu’à 15 s budget total (hors delay).
@CircuitBreaker paramétré
Pour un appel coûteux et incertain — par exemple un appel à un LLM externe :
@ApplicationScoped
public class LlmClient {
@CircuitBreaker(
requestVolumeThreshold = 4, // observe les 4 derniers appels
failureRatio = 0.75, // ouvre à 75 % d'échec (3 sur 4)
delay = 30_000, // attend 30 s en état OPEN
successThreshold = 2 // 2 sondes OK pour refermer
)
@Timeout(10_000)
@Fallback(fallbackMethod = "fallbackCompletion")
public Completion complete(Prompt prompt) {
return llm.complete(prompt);
}
private Completion fallbackCompletion(Prompt prompt) {
return Completion.canned("Service temporairement indisponible.");
}
}
Combinatoire à éviter
@Asynchronous sur une méthode qui retourne void ou un type autre que CompletionStage / Future → FaultToleranceDefinitionException au démarrage.
// REJETÉ par la BCE Vauban
@Asynchronous
public void fireAndForget() { /* ... */ } // void interdit, voir spec §6.1
@Bulkhead(value=0) est rejeté par la spec (§9.2).
@Retry(maxRetries = -1) autorise un nombre infini de tentatives ; toujours combiner avec @Retry(maxDuration = …) ou @Timeout au-dessus pour borner le pire cas.
Coordonnées Maven
<dependency>
<groupId>io.vidocq.heisenberg</groupId>
<artifactId>heisenberg-cdi-vauban</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
Modules JPMS correspondants : io.vidocq.heisenberg.core et io.vidocq.heisenberg.cdi.vauban. La SPI publique vit dans io.vidocq.heisenberg.api.*.
Surcharges de configuration sans recompilation
Tous les paramètres sont surchargeables via Ravel (MicroProfile Config) en suivant la précédence standard MP FT 4.1 §12 : méthode > classe > valeur globale par défaut.
# Globale — tout @Retry de l'application
fault-tolerance/Retry/maxRetries=5
# Par classe — toutes les méthodes de CatalogClient
fault-tolerance/io.example.CatalogClient/Retry/maxRetries=2
# Par méthode — prioritaire absolu
fault-tolerance/io.example.CatalogClient/fetch/Retry/maxRetries=1
fault-tolerance/io.example.CatalogClient/fetch/CircuitBreaker/delay=60000
# Coupe la tolérance hors @Fallback (utile en intégration)
fault-tolerance/MP_Fault_Tolerance_NonFallback_Enabled=false
Tester sans container
Les moteurs heisenberg-core sont purs Java — utilisables sans CDI dans un test JUnit 5 pour valider la logique d’une politique sans déployer l’application.
import io.vidocq.heisenberg.core.RetryEngine;
import io.vidocq.heisenberg.api.RetryConfig;
class RetryEngineTest {
@org.junit.jupiter.api.Test
void retriesUpToMaxRetries() {
var cfg = RetryConfig.builder().maxRetries(3).delayMillis(0).build();
var engine = new RetryEngine(cfg);
// engine.execute(() -> { ... }) — voir Javadoc
}
}
Pour les tests d’intégration CDI, utiliser Vauban embedded plutôt qu’Arquillian — voir Vauban.
Pour aller plus loin
-
Concepts — ordre de composition, états CircuitBreaker, sémantique async.
-
Référence — paramètres complets de chaque annotation, table de clés MP Config.
-
Fonctionnement interne — virtual threads, StateRegistry, métriques.