La dernière fois, on s'est quittés sur une question laissée en suspens.
Un @Timeout(1) et un @Fallback, posés proprement sur une méthode. À la lecture, tout y est. Mais le timeout se déclenche-t-il vraiment ? Le fallback prend-il le relais ? Et si un @Retry s'en mêle, combien de fois l'appel est-il rejoué ?
J'avais promis d'y répondre en lâchant le gobelin dans la taverne de The Falling Whale.
C'est l'heure.
Le principe tient en une phrase : la présence d'un mécanisme de résilience n'est pas la preuve de son efficacité.
Dans l'article sur la naissance de Goblin c'était une conviction. Cette nuit, ce sera une mesure.
Une taverne qui n'a jamais vu un gobelin
Dans La taverne en flammes, chaque panne était codée à la main : une cuisine qui ralentit sur commande, un grimoire qu'on met en panne par un endpoint dédié, un feu qu'on allume avec un curl. Faute de Chaos Monkey pour Quarkus, il fallait bien fabriquer le chaos soi-même. Et je concluais sur un « pas encore » qui n'était pas anodin.
Cette fois, la taverne est une application ordinaire. Elle ne sait pas qu'elle va être attaquée, et elle ne contient pas une ligne de code de chaos.
Le dépôt quarkus-goblin-demo reprend les briques de la série, assemblées dans une seule maison :
| Salle de la taverne | Contenu |
|---|---|
| Le comptoir | 5 ressources REST : /grimoire, /cave, /aventuriers, /commandes, /guilde-des-marchands |
| Les cuisines | 6 services, dont OrderService, GrimoireService, CellarService et MerchantService |
| La cave | Hibernate ORM Panache, 5 repositories et PostgreSQL |
| La porte de derrière | Un REST Client vers la guilde des marchands, pour avoir un véritable appel sortant |
| La carte des pièges | Gestion centralisée des erreurs, avec toutes les réponses au format application/problem+json |
| Le menu affiché | OpenAPI et Swagger UI |
| Les vieilles lois | SmallRye Fault Tolerance : @Timeout, @Retry, @Fallback, @CircuitBreaker, @RateLimit |
| La salle | L'écran de démo, qui montre en direct ce que vivent les clients |
| La tour de guet | Micrometer et OpenTelemetry, Prometheus et Grafana, ainsi que Tempo pour les traces et Loki pour les logs |
Autour de l'application, un docker-compose.yml monte toute la tour de guet :
Un détail qui a son importance : l'application tourne sur la machine, en dev mode, et non dans un conteneur. C'est le seul endroit où le gobelin existe. Dans un build de production, il n'y a tout simplement rien à désactiver.
Faire entrer le gobelin
Trois dépendances : le gobelin lui-même, et ses deux modules optionnels, pour qu'il parle à Prometheus et à OpenTelemetry.
<dependency>
<groupId>io.quarkiverse.goblin</groupId>
<artifactId>quarkus-goblin</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.goblin</groupId>
<artifactId>quarkus-goblin-metrics</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.goblin</groupId>
<artifactId>quarkus-goblin-opentelemetry</artifactId>
<version>0.3.0</version>
</dependency>Et quelques lignes de configuration. Au démarrage, le gobelin se contente de traîner des pieds : 50 à 400 ms de latence sur un quart des requêtes.
quarkus.goblin.enabled=true
quarkus.goblin.assault.type=LATENCY
quarkus.goblin.assault.latency.min-milliseconds=50
quarkus.goblin.assault.latency.max-milliseconds=400
quarkus.goblin.target.level=25
quarkus.goblin.target.include-packages=fr.eletutour.tavern
# pas de chaos dans l'infrastructure : mappers d'erreurs, métriques et traces, données initiales
quarkus.goblin.target.exclude-packages=fr.eletutour.tavern.exception,fr.eletutour.tavern.observability,\
fr.eletutour.tavern.bootstrapOn verra plus loin qu'un dernier paquet vient s'ajouter à cette liste.
Pour ouvrir la taverne, l'ordre compte : la tour de guet d'abord, parce que l'application a besoin de sa cave pour démarrer, et qu'elle envoie ses traces et ses logs au collecteur dès le premier instant.
docker compose up -d
./mvnw quarkus:dev
./scripts/trafic.shLe dernier script joue les clients : il commande, consulte le grimoire, réapprovisionne la cave, et se trompe de temps en temps de porte. Il ne reste qu'à ouvrir la salle, sur http://localhost:8080/salle, et la Dev UI juste à côté : c'est de là que se pilote le gobelin.

La salle : voir la taverne tenir
Les dashboards Grafana convainquent un public d'ops. Mais devant une salle de conférence, une courbe qui monte n'émeut personne : pour croire à la résilience, le public doit voir la taverne souffrir et tenir, sans avoir à déchiffrer un axe ou une légende.
La démo a donc sa propre salle : un écran à projeter, qui montre ce que vivent les clients de la taverne.

| Panneau | Contenu |
|---|---|
| Le gobelin | S'il rôde, ce qu'il injecte et sur quelle part des requêtes |
| Les compteurs | Les commandes servies, refusées et perdues, ainsi que le temps de service moyen |
| Le comptoir | Chaque commande, telle que le client la vit : verte si elle est servie, orange si elle est refusée, rouge si elle est abandonnée ou perdue |
| La carte du jour | Elle se transforme en ardoise écrite à la craie lorsque le grimoire sert sa dernière carte connue |
| La porte de derrière | La lanterne du disjoncteur de la guilde (verte, rouge ou orange) et les chariots qui arrivent — ou pas |
| Les bourses | L'or des aventuriers, qui baisse à chaque commande servie et ne bouge pas d'une pièce lorsqu'une commande est annulée |
La mise en scène devient simple : la salle en grand, la Dev UI à côté. On clique dans la Dev UI, la salle réagit. Grafana, Tempo et Loki n'arrivent qu'ensuite, pour expliquer pourquoi.
Côté technique, la salle reprend deux briques de la série : Qute pour rendre la page, et les Server-Sent Events pour la faire vivre sans jamais la recharger.
@GET
@Path("/evenements")
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<SalleEvent> evenements() {
return salle.flux();
}Surtout, la salle ne simule rien. Elle observe la taverne :
- les services publient de vrais événements du domaine, par exemple la carte servie depuis la cave ou depuis l'ardoise ;
- l'état du disjoncteur est suivi par son nom, grâce à
CircuitBreakerMaintenancede SmallRye Fault Tolerance ; - les commandes sont regardées à la sortie du comptoir par un filtre JAX-RS ;
- la page elle-même ne lit jamais la base : la carte et les bourses sont chargées à l'ouverture de la taverne, puis tenues à jour par les commandes servies. Sans ça, c'est l'écran de démo qui tomberait le premier quand le gobelin s'attaque à la cave.
Ce dernier point n'est pas un détail : une commande coupée par un @Timeout ne sort jamais du service, seule la réponse HTTP la raconte.
@Override
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
if (request.getProperty(DEBUT) instanceof Long debut) {
salle.commande(response.getStatus(), response.getEntity(), (System.nanoTime() - debut) / 1_000_000);
}
}Dernière précaution : la salle est exclue du ciblage du gobelin. Sans cela, il ralentirait aussi l'écran de démo, ce qui ferait rire la salle, mais pas pour les bonnes raisons.
quarkus.goblin.target.exclude-packages=fr.eletutour.tavern.exception,fr.eletutour.tavern.observability,\
fr.eletutour.tavern.bootstrap,fr.eletutour.tavern.salleLe piège de la porte d'entrée
Avant de lâcher le gobelin sur le fameux @Timeout, il faut comprendre un piège. Je suis moi-même tombé dedans.
Si le gobelin frappe à la porte de la taverne, c'est-à-dire au niveau du filtre REST, la panne arrive avant que la méthode protégée ne s'exécute. Le @Timeout de OrderService ne voit rien passer : la requête est déjà ralentie ou déjà en erreur avant même d'atteindre le service. On teste alors la patience du client, pas la résilience de la taverne.
C'est pour cela que Goblin raisonne en couches :
| Couche | Où frappe le gobelin |
|---|---|
DATABASE |
à l'acquisition d'une connexion JDBC, sous Hibernate et Panache |
MESSAGING |
sur les consommateurs @Incoming |
SERVICE |
sur les beans applicatifs, à l'intérieur de Fault Tolerance |
HTTP_OUT |
sur les appels sortants du REST Client |
HTTP_IN |
à la porte d'entrée, sur les ressources REST |
La couche SERVICE est la clé de notre question. Son intercepteur est placé à la priorité 4100, alors que celui de Fault Tolerance est à 4010 : il s'exécute dans l'enveloppe du @Timeout, du @Retry et du @Fallback. Cette fois, la panne injectée est exactement celle que les annotations sont censées absorber.
Une dernière règle à connaître : seul l'appel de bean le plus extérieur d'une requête est attaqué. Les annotations de résilience doivent donc vivre sur les services que les ressources appellent directement, sinon le gobelin frappe à côté.
Premier soir : le service ordinaire
Le trafic tourne depuis trois minutes. La taverne sert un peu plus de 9 requêtes par seconde, sans une seule erreur serveur.

La courbe des commandes en échec intrigue au premier regard. Ce ne sont pas des pannes, ce sont des refus métier. Bilbon n'a jamais un sou en poche, et l'Élixir du Phénix est vite en rupture : la cave ne compte que cinq queues de phénix. Le comptoir refuse proprement, et le dit.
{
"type": "urn:falling-whale:problem:bourse-insuffisante",
"title": "Bourse insuffisante",
"status": 422,
"detail": "Bilbon n'a pas assez de pièces d'or pour régler la commande.",
"code": "bourse-insuffisante",
"bourse": 0,
"prix": 320,
"traceId": "2922c679829d825653ab824344f23982"
}Et le gobelin, lui, est déjà au travail. Dans Tempo, la trace d'une commande servie montre son passage : 275 ms de goblin.assault sur les 330 ms de la requête, juste avant que la cuisine ne prépare la recette et que la cave ne soit sollicitée. Chaque span porte une petite icône : un clic affiche les logs de cette étape.

Deuxième soir : la cuisine traîne
Place à la question du jour. Le comptoir des commandes a sa loi : une commande doit être servie en moins d'une seconde et demie.
@Transactional
@Timeout(1500)
@RateLimit(value = 60, window = 10, windowUnit = ChronoUnit.SECONDS)
@BusinessTimed(value = "tavern.order", description = "Commandes passées au comptoir")
public OrderDTO passerCommande(OrderRequest request) {
...
}Dans la Dev UI, on arme la seule couche Service, avec une latence de 1 800 à 2 000 ms. Le gobelin ralentit chaque commande au-delà du seuil.
Dans la salle, le comptoir vire au rouge. Les commandes s'arrêtent toutes au même endroit, entre 1 504 et 1 508 ms : abandonnées. Et du côté des bourses, rien ne bouge. Pas une pièce d'or ne quitte la poche d'un aventurier.

Côté client, la réponse arrive en 1 514 ms :
{
"type": "urn:falling-whale:problem:fault-tolerance",
"title": "Service trop lent",
"status": 504,
"detail": "La cuisine n'a pas servi la commande à temps.",
"faultTolerance": "TimeoutException",
"traceId": "ef70f9c8703c34bbbe05793acf7822bb"
}Le @Timeout s'est bien déclenché. La trace le montre sans ambiguïté : le span goblin.assault est coupé net à 1 505 ms, alors que le gobelin en réclamait près de deux secondes.

Et les bourses intactes de la salle ont leur explication : la transaction de la commande est annulée. L'aventurier n'est pas débité d'une commande qu'on ne lui a jamais servie. Sur le dashboard, la latence des commandes vient buter sur le plafond du timeout, et l'alerte TavernOrderLatencyP95High passe en firing, accompagnée de TavernErrorRateHigh : un 504 reste une erreur serveur.


Le @Timeout du premier article a enfin sa réponse : oui, il se déclenche. Et on sait maintenant à quelle milliseconde.Troisième soir : la cave tombe
Cette fois, le gobelin descend à la cave. On arme la seule couche Database, avec des exceptions, au niveau 100 % : chaque acquisition de connexion JDBC échoue.

Le grimoire a prévu le coup. Chaque fois qu'il sert la carte, il en garde une copie sur une ardoise, au-dessus du comptoir. Si la cave ne répond plus, il réessaie deux fois, puis lit l'ardoise.
@Retry(maxRetries = 2, delay = 100)
@Fallback(fallbackMethod = "lireLArdoise")
@WithSpan("consulter-le-grimoire")
public List<RecipeDTO> consulterLeGrimoire() {
List<RecipeDTO> carte = recipeRepository.listAllWithIngredients().stream().map(DtoMapper::toDto).toList();
ardoise.set(carte);
return carte;
}
List<RecipeDTO> lireLArdoise() {
List<RecipeDTO> carte = ardoise.get();
LOG.warnf("La cave ne répond plus : le tavernier lit l'ardoise (%d recette(s))", carte.size());
return carte;
}Et c'est exactement ce qui se passe. Dans la salle, la carte du jour se change en ardoise : les recettes s'écrivent à la craie, et un bandeau prévient que la cave ne répond plus. La carte reste lisible, le client peut toujours choisir. Le comptoir, lui, se remplit de commandes perdues.

GET /grimoire/recettes continue en effet de répondre en 200, avec toute la carte. La trace raconte l'histoire mieux qu'un long discours : trois goblin.assault en erreur (la première tentative et les deux nouvelles tentatives), puis le repli.

Le reste de la taverne, en revanche, n'a pas d'ardoise. Les commandes, les réapprovisionnements et le registre des aventuriers ont besoin de la cave : 81,9 % des requêtes finissent en erreur serveur, proprement, en problem+json. Le dashboard du gobelin montre la bascule, de la latence à la porte d'entrée vers les exceptions dans la cave, et la montée des replis servis par le grimoire.

Côté tour de guet, l'alerte TavernErrorRateHigh se déclenche, à côté de GoblinChaosActive, qui rappelle en permanence qu'un gobelin rôde.

Quatrième soir : le marchand ne vient pas
Dernière attaque, sur la porte de derrière. La cave se réapprovisionne auprès de la guilde des marchands, par le REST Client. On arme la seule couche Outbound HTTP, avec des exceptions côté client : le coursier ne part même plus.
Le service de la guilde est protégé par toutes les vieilles lois à la fois :
@Timeout(2000)
@Retry(maxRetries = 2, delay = 200, abortOn = CircuitBreakerOpenException.class)
@CircuitBreaker(requestVolumeThreshold = 6, failureRatio = 0.5, delay = 10_000)
@Fallback(fallbackMethod = "marchandAbsent")
@WithSpan("commander-a-la-guilde")
public Delivery commander(String ingredient, int quantity) {
return merchantGuild.deliver(new DeliveryRequest(ingredient, quantity));
}
Deux nouvelles tentatives, puis le repli : la cave répond MARCHAND_ABSENT, sans erreur. Après quelques échecs, le disjoncteur s'ouvre et n'envoie plus de coursier pendant dix secondes.
Dans la salle, la lanterne de la porte de derrière passe au rouge, et la liste des livraisons se remplit de marchands absents. Au comptoir, en revanche, la vie continue : les commandes sont servies, comme si de rien n'était. Du moins pour un temps. Sans livraisons, la cave se vide peu à peu, et les premières ruptures de stock apparaissent au comptoir. La panne de la porte de derrière finit par se voir dans la salle : c'est une défaillance en cascade, exactement le genre de chose qu'on ne découvre qu'en laissant le chaos durer.

Dans Grafana, le panneau dédié montre le même disjoncteur passer de closed à open.

Une fois le disjoncteur ouvert, la réponse arrive en 5 ms :
{
"stock": {
"ingredientId": 3,
"ingredientName": "Eau de source elfique",
"unit": "litre",
"quantity": 350
},
"delivered": 0,
"status": "MARCHAND_ABSENT",
"message": "La guilde des marchands n'a pas répondu, réessayez plus tard."
}Cinq millisecondes. Ce n'était pas le cas la première fois.
Regardez l'abortOn du @Retry. Il n'était pas là au départ. Lors du premier passage du gobelin, le disjoncteur s'ouvrait bien, mais chaque appel refusé mettait encore 394 ms à répondre. Le @Retry réessayait aussi les refus du disjoncteur ouvert : deux nouvelles tentatives de 200 ms chacune, pour obtenir deux fois le même refus avant d'enfin servir le repli. Un disjoncteur qui fait patienter ceux qu'il refuse perd l'essentiel de son intérêt.
Aucun test n'avait vu le problème : le comportement était correct, seulement lent. Il a fallu du trafic, un gobelin et un chronomètre pour le voir.
C'est tout l'intérêt de l'exercice : le chaos n'a pas seulement validé nos défenses, il en a trouvé une qui ne faisait pas son travail.
Les logs racontent la soirée
Métriques et traces disent combien et où. Il manquait le pourquoi : les logs.
L'application les envoie en OTLP au collecteur, qui les transmet à Loki. Chaque ligne garde son trace_id : depuis un log, un clic ouvre sa trace dans Tempo, et depuis une trace, un clic affiche ses logs. En bas du dashboard du gobelin, le carnet du gobelin consigne chaque panne injectée, en regard de ce que la taverne en dit.

Les logs métier complètent le tableau, en français : commandes servies avec la bourse restante, livraisons de la guilde, refus métier, erreurs serveur.

Et c'est en lisant ces logs que le gobelin s'est fait prendre à son propre jeu. Pendant la panne de la cave, Agroal, le pool de connexions, répétait le même avertissement à chaque exception injectée :
Datasource '<default>': Closing connection in incorrect state CHECKED_INChaque faute injectée détruisait une connexion au lieu de la rendre au pool, et le compteur des connexions actives n'était jamais décrémenté : 1 324 connexions « actives » et -1 316 « disponibles » après quelques minutes, et 677 connexions créées puis détruites pour rien. Le pool continuait de servir, mais ses métriques étaient faussées pour de bon. Un outil de chaos a lui aussi besoin d'être éprouvé : la correction est arrivée dans Goblin 0.3.1.
Le lendemain : la main courante et le post-mortem
Dans La taverne en flammes, le lendemain de l'incendie, le tavernier d'astreinte ne cherchait pas un coupable. Il tirait un post-mortem de ce qui avait été enregistré pendant la nuit, et rien d'autre. Le sans-blâme n'y était pas une posture, c'était une contrainte de conception.
La démo reprend le mécanisme, avec un avantage de taille : cette fois, la taverne a une vraie base PostgreSQL. Le journal de la nuit peut donc survivre à la nuit.
La taverne tient une main courante, le registre où l'exploitation consigne tout ce qui se passe pendant un incident. Personne ne la remplit à la main :
- un incident s'ouvre au premier signe de dégradation qu'un client ou la résilience peut voir : une commande perdue ou abandonnée, une carte servie depuis l'ardoise, une livraison manquée, un disjoncteur qui s'ouvre ;
- pendant l'incident, chaque fait est noté : les commandes, les replis, les livraisons, les changements d'état du disjoncteur ;
- il se clôt après trente secondes de calme, disjoncteur refermé.
Le gobelin de fond, celui qui ajoute un peu de latence sur un quart des requêtes, n'ouvre jamais d'incident. Une soirée ordinaire ne remplit pas le registre.
Le témoin des méfaits du gobelin
Un post-mortem qui écrit « cause inconnue » ne sert pas à grand-chose. Or ici, la cause est connue : c'est le gobelin. Encore faut-il qu'il laisse une trace.
Goblin expose un point d'extension, AssaultObserver, celui-là même qu'utilisent ses modules de métriques et de traces. Il suffit d'un bean pour que chaque assaut, et chaque réveil ou coucher du gobelin, rejoigne la main courante :
@ApplicationScoped
public class TemoinDuGobelin implements AssaultObserver {
@Inject
MainCourante mainCourante;
@Override
public void onAssault(AssaultEngine.AssaultRecord assaut) {
mainCourante.noter(TypeDeFait.ASSAUT, assaut.sourceTag() + "/" + assaut.type(),
assaut.type() + " sur " + assaut.method() + " (" + assaut.configSnapshot() + ")",
assaut.latencyMs() > 0 ? assaut.latencyMs() : null);
}
}Ne pas brûler avec la taverne
Il y a un piège, et il est de taille : un registre qu'on écrit en base pendant que le gobelin attaque la base.
Si la requête qui perd une commande écrivait elle-même dans la main courante, son écriture passerait par la même connexion JDBC que celle que le gobelin vient de faire échouer. Le registre brûlerait avec la taverne.
Les faits sont donc d'abord posés dans une file en mémoire, sans jamais bloquer ni échouer. Une tâche planifiée les consigne ensuite en base, toutes les deux secondes :
@Scheduled(every = "${taverne.main-courante.consignation:2s}", concurrentExecution = SKIP)
public synchronized void consigner() {
// vide la file, ouvre ou clôt l'incident, écrit les faits en une transaction ;
// si la base ne répond plus, les faits attendent la consignation suivante
}La tâche tourne hors de toute requête HTTP, elle n'a donc aucune couche de chaos résolue : le gobelin ne la touche jamais. Et si la base tombe pour de vrai, les faits patientent en mémoire.
Survivre à la nuit
Pour la démo, la cave est recréée à chaque démarrage : les bourses et les étagères repartent de zéro, c'est voulu. La main courante, elle, doit tout garder.
Elle a donc sa propre unité de persistance Hibernate, dans son propre schéma PostgreSQL, et son schéma est versionné par Flyway :
quarkus.hibernate-orm."main-courante".datasource=<default>
quarkus.hibernate-orm."main-courante".packages=fr.eletutour.tavern.incident
quarkus.hibernate-orm."main-courante".database.default-schema=main_courante
quarkus.hibernate-orm."main-courante".schema-management.strategy=none
quarkus.flyway.migrate-at-start=true
quarkus.flyway.schemas=main_couranteLe premier essai laissait Hibernate gérer les deux schémas, en `drop-and-create` pour la cave et en `update` pour la main courante. Au premier rechargement du dev mode, l'application refusait de redémarrer : les deux unités démarrent en parallèle, et la seconde lisait les tables de la cave pendant que la première les supprimait. Avec Flyway, la main courante est prête avant même qu'Hibernate ne démarre, et plus rien ne se croise.
Un incident en cours au moment d'un redémarrage n'est pas perdu non plus : il est marqué INTERROMPU, avec l'heure du redémarrage.
Pendant l'incident, la salle l'affiche en pied de page. Une fois le calme revenu, elle donne le lien vers le post-mortem.

Le post-mortem
Le lendemain, il suffit de le demander, en JSON ou en Markdown, prêt à coller dans le compte rendu de l'équipe :
curl -s -H 'Accept: text/markdown' localhost:8080/exploitation/incidents/1/post-mortemVoici celui d'une soirée où la cave a pris feu une requête sur deux, avant que la guilde des marchands ne réponde plus, sous le trafic habituel. En voici les rubriques principales :
## Impact
| Mesure | Valeur |
|---|---|
| Commandes servies | 55 |
| Commandes perdues (5xx) | 26 |
| Cartes servies depuis l'ardoise | 4 |
| Livraisons manquées | 9 |
| Disjoncteur ouvert | 20 s |
| Assauts du gobelin | 99 |
## Cause probable
- Le gobelin a injecté 61 exception (source database), par exemple : exception sur Database <default> connection
- Le gobelin a injecté 4 exception (source rest-client), par exemple : exception sur REST-Client POST .../guilde-des-marchands/livraisons
## Ce qui a fonctionné
- La carte est restée servie : 4 fois depuis l'ardoise, la dernière carte connue (@Retry puis @Fallback du grimoire)
- Chaque réapprovisionnement sans livraison a reçu une réponse (repli MARCHAND_ABSENT) : 9 au total, et le disjoncteur a isolé la guilde pendant 20 s
## Ce qui n'a pas fonctionné
- 26 commande(s) perdue(s) en erreur serveur : ces clients n'ont reçu aucun repli
## Actions correctives
- Les commandes n'ont aucun repli quand la cave tombe : répondre 503 avec un Retry-After, ou mettre les commandes en attente plutôt que de les perdre
- Relancer les réapprovisionnements manqués une fois la guilde revenue : aucune relance automatique n'existe aujourd'huiChaque phrase découle d'un fait compté. Aucune n'est écrite à la main pour un incident donné, et aucune ne dit qui a fait quoi.
Le post-mortem nomme au passage une faiblesse que le troisième soir montrait sans la nommer, avec son comptoir rempli de commandes perdues : quand la cave tombe, la carte a son ardoise, la guilde son repli, mais les commandes n'ont rien. Elles échouent en 500. C'est le prochain chantier de la taverne.
Rallumer le feu
Reste une dernière rubrique, celle qui ferme la boucle. À l'ouverture de l'incident, la main courante a aussi noté la configuration exacte du gobelin. Le post-mortem la rend sous la forme d'une commande prête à l'emploi :
node scripts/gobelin.mjs armer '{"layers":["DATABASE"],"exceptionEnabled":true,"level":50,...}' && node scripts/gobelin.mjs actif onLe correctif est en place ? On rallume le même feu, et le post-mortem suivant dira s'il tient.
Le gobelin en CI
Tout ceci se joue à la main, en direct, et c'est ce qui en fait un excellent support de démonstration. Mais rien n'empêche de verrouiller ces comportements dans des tests.
Par défaut, Goblin reste inactif dans les tests : ajouter l'extension ne ralentit ni ne casse une suite existante. Un @TestProfile suffit pour l'autoriser dans une classe dédiée :
public class ChaosTestProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of("quarkus.goblin.test.enabled", "true");
}
}Le deuxième soir devient alors un test comme un autre :
@Test
void uneCuisineTropLenteDeclencheLeTimeout() {
config.setLayers(Set.of(ChaosLayer.SERVICE));
config.setLatencyEnabled(true);
config.setLatencyRange(1800, 2000);
int adventurer = OrderResourceTest.inscrire("Legolas", 100);
given().contentType(ContentType.JSON)
.body(OrderResourceTest.commande(adventurer, OrderResourceTest.recette("Tartine du voyageur"), 1))
.when().post("/commandes")
.then().statusCode(504)
.contentType("application/problem+json")
.body("faultTolerance", equalTo("TimeoutException"));
// la transaction de la commande est annulée
given().when().get("/aventuriers/" + adventurer).then().body("gold", equalTo(100));
}La démo en compte quatre de ce type :
- l'ardoise quand la cave tombe ;
- le
504quand la cuisine traîne ; - le marchand absent ;
- et le disjoncteur ouvert qui doit répondre sans attendre. Celui-là est né du quatrième soir, pour que l'oubli de l'
abortOnne revienne jamais.
Conclusion
On avait mis des défenses partout. Un timeout, des retries, des replis, un disjoncteur. Sur le papier, la taverne était imprenable.
Quatre soirées de gobelin plus tard, voici ce qu'on sait vraiment, et ce que le public a vu de ses propres yeux :
- le
@Timeoutcoupe la commande à 1,5 s, et la transaction est annulée : pas une pièce d'or ne quitte les bourses ; - le
@Retryet le@Fallbackdu grimoire tiennent la carte ouverte pendant que la cave est en panne ; - le repli de la guilde fonctionne, mais le disjoncteur ne servait à rien tant que le
@Retryréessayait ses refus, et une panne du fournisseur finit par vider la cave ; - et même l'outil de chaos avait un défaut, visible seulement dans les logs.
- le lendemain, la main courante en tire un post-mortem chiffré, sans blâme, qui désigne le maillon faible suivant : les commandes, sans repli quand la cave tombe.
Dans Application à toute épreuve, la forteresse se construisait en trois couches : concevoir, éprouver, mesurer. Côté Spring Boot, Chaos Monkey tenait le rôle du bélier. Côté Quarkus, la place était vide.
Elle ne l'est plus.
La résilience ne devrait pas seulement être déclarée. Elle devrait être éprouvée, et désormais, on peut le faire sans toucher une ligne du code métier.
La prochaine étape est déjà sur la table : laisser le gobelin vérifier lui-même le résultat de ses assauts. Déclarer « si j'injecte 2 s de latence sur les commandes, j'attends un 504 en moins de 1,6 s », et obtenir un verdict. Mais c'est une autre histoire, pour une autre soirée à la taverne.
Tout le code de la démo, le compose et les dashboards sont ici :
Les sources de quarkus-goblin sont ici en open-source
et toute la documentation de l'outils est consultable ici



