Une règle d'automatisation a envoyé 32 094 emails à 7 clients : anatomie d'une panne
Sept tickets. Trente-deux mille quatre-vingt-quatorze notifications. Et personne pour s'en apercevoir.
Le 8 septembre, en ouvrant l'onglet Activité d'un ticket sur notre environnement de recette, nous sommes tombés sur un compteur de notifications à cinq chiffres. Le ticket avait sept jours. La règle d'automatisation qui les avait produites avait tourné toutes les cinq minutes, sans relâche, et avait renvoyé le même email à chaque passage.
Nous publions ce post-mortem pour trois raisons : la cause est un piège de conception que porte n'importe quel moteur de règles, le correctif tient en deux idées, et ce que la panne apprend sur la détection vaut plus que le correctif lui-même.
Les faits
| Environnement | Recette, données de démonstration, aucun destinataire réel |
| Durée | Environ sept jours |
| Population touchée | 7 tickets |
| Notifications produites | 32 094 |
| Détection | À l'œil, en ouvrant l'activité d'un ticket — aucune alerte |
| Impact client | Nul. Sur un environnement de production, la réputation d'expéditeur du domaine était en jeu |
La cause : un déclencheur planifié n'a pas de mémoire
Un moteur de règles distingue deux familles de déclencheurs, et la différence entre les deux est exactement l'endroit où l'on tombe.
- Sur événement — « quand un ticket passe en urgent ». Le déclencheur est un fait daté. Il se produit une fois, la règle s'exécute une fois.
- Planifié — « toutes les cinq minutes, sur les tickets sans réponse depuis 4 heures ». Le déclencheur n'est pas un événement : c'est une question posée à la base de données, et la base répond la même chose à chaque passage.
Notre règle était de la seconde famille. Elle demandait « quels tickets attendent une première réponse depuis plus de quatre heures ? », recevait sept identifiants, envoyait sept rappels, et oubliait immédiatement l'avoir fait. Cinq minutes plus tard, la même question, la même réponse, sept rappels de plus. Trois cent trente-six passages par jour, sept jours.
-- La question, posée toutes les 5 minutes
select id from tickets
where first_reply_at is null
and created_at < now() - interval '4 hours';
-- 7 lignes. Toujours les 7 mêmes.
-- Rien dans cette requête ne dit "et que je n'ai pas déjà notifiés".Le piège est là, et il est joli : la requête est correcte. Elle répond exactement à ce qu'on lui demande. C'est la question qui était incomplète, et une question incomplète ne lève aucune erreur.
Pourquoi ça a duré sept jours
Une règle qui boucle produit un signal fort — trente-deux mille lignes — et pourtant invisible, parce que rien ne regardait au bon endroit. Trois raisons se sont additionnées :
- 1L'envoi fonctionnait. Aucune erreur, aucun échec, aucune file bloquée. Du point de vue du système, trente-deux mille emails partis avec succès sont trente-deux mille succès.
- 2Le volume est réparti. Sept tickets, sept destinataires : chacun recevait « seulement » quelques dizaines d'emails par jour. Aucun seuil par destinataire n'était franchi.
- 3Personne ne surveillait un débit. Nous avions des sondes de disponibilité et des compteurs d'erreur. Nous n'avions pas de compteur du nombre d'actions produites par une règle, qui est précisément la grandeur qui explose ici.
Un mécanisme qui boucle ne se voit pas dans un échantillon : il se voit dans un compteur.
En regardant un ticket, on voit une longue liste d'activité et l'on se dit que le ticket est agité. En regardant dix tickets, on voit dix tickets agités. Le défaut n'apparaît que quand on cesse de regarder des tickets pour regarder un total — et il n'y avait pas de total.
Le correctif : deux idées, dans cet ordre
1. L'idempotence par ticket et par règle
Une règle planifiée doit se souvenir de ce qu'elle a déjà fait, et ce souvenir doit être porté par le couple règle-cible, pas par la règle seule. Une règle qui note « j'ai tourné » ne peut plus jamais notifier un huitième ticket. Une règle qui note « j'ai notifié le ticket 4800 » peut encore traiter le 4801, et ne renotifiera pas le 4800.
Le corollaire, moins évident : ce souvenir a besoin d'une condition de réarmement explicite. « Rappeler quand aucune réponse depuis 4 heures » ne doit pas se réarmer tout seul ; il doit se réarmer quand quelque chose de nouveau se produit — une réponse, un changement d'assignation. Sans réarmement décidé, on remplace une boucle par un silence, ce qui est un autre bug.
2. Un plafond par exécution
L'idempotence corrige cette cause. Le plafond corrige les causes qu'on n'a pas encore trouvées. Une exécution qui s'apprête à produire plus de N actions s'arrête, consigne, et alerte — parce qu'une règle qui touche soudain quatre mille tickets a probablement un défaut, même si sa logique est correcte.
Ce que nous avons changé au-delà de la règle
Corriger le bug était l'affaire d'un après-midi. Le reste a pris plus longtemps, et c'est le reste qui compte.
- Un compteur sur les files en échec, avec deux règles d'alerte : une au premier échec persistant quinze minutes, une seconde, critique, au-delà de vingt. Il ne faut pas confondre « ça a échoué » et « ça échoue en boucle ».
- Une politique de reprise sur les balayages périodiques. Nous avons découvert, en cherchant autre chose, qu'aucune de nos files n'en avait : la moindre coupure pendant un déploiement laissait un travail en échec définitivement, ce qui allumait l'alerte à demeure. Une alerte rouge en permanence est ignorée en une semaine — et l'on revient exactement à l'aveuglement de cet incident.
- Une borne sur les travaux terminés. Toujours en cherchant autre chose : rien ne les purgeait. Une file qui marche ressemble exactement à une file qui fuit.
Le fil rouge de ces trois corrections n'est pas la fiabilité, c'est la détection. Sur les sept défauts que nous avons ouverts cette semaine-là, cinq étaient des défauts de détection et non de protection : le système faisait ce que nous croyions, sauf là où personne ne regardait.
Ce que vous pouvez vérifier chez vous cette semaine
Quel que soit votre outil, quatre questions se posent en dix minutes et se répondent en regardant, pas en supposant.
- 1Combien d'actions vos règles ont-elles produites hier ? Si vous ne pouvez pas répondre par un nombre, vous êtes dans la même situation que nous l'étions.
- 2Vos règles planifiées savent-elles ce qu'elles ont déjà traité ? Cherchez la notion de « déjà notifié » dans leur configuration. Si elle n'existe pas, la boucle est possible.
- 3Qu'est-ce qui réarme un rappel ? Si la réponse est « rien », il boucle. Si la réponse est « je ne sais pas », il boucle probablement.
- 4Que se passe-t-il si une règle touche mille tickets d'un coup ? S'il n'y a pas de plafond, la réponse est : mille actions, et vous l'apprendrez par vos clients.
Notre moteur de règles, ses garde-fous et ses tests sont lisibles : le code est sous licence AGPL-3.0. Voir les règles et SLA pour ce que le moteur fait, et la page open source pour aller lire comment.
Nous publions nos pannes parce qu'un éditeur qui n'en publie jamais n'en a pas moins — il en parle moins. Et parce que celle-ci, sur un environnement de production avec de vrais destinataires, aurait coûté bien plus qu'un compteur à cinq chiffres.
Nous ne publions pas d'article sans un chiffre mesuré chez nous, du code qu'on peut aller lire, ou une limite avouée. C'est la seule façon d'écrire sur ce métier sans répéter ce que tout le monde écrit déjà.