À la fin du dernier article, nous avions quelque chose qui semblait remarquablement peaufiné.
- Un client envoie un message.
- Il apparaît dans Slack.
- Un représentant du soutien répond.
- La réponse est enregistrée dans notre application.
- Le navigateur du client se met à jour presque instantanément grâce aux événements envoyés par le serveur.
- Tout fonctionne.
Et c'est exactement là que beaucoup de projets s'arrêtent.
Malheureusement, les systèmes de production, eux, ne s'arrêtent pas.
L'une des plus grosses erreurs que les développeurs font lorsqu'ils créent des intégrations est de tout concevoir autour du scénario idéal. C'est plaisant. Tout répond immédiatement. Les réseaux coopèrent. Les API sont disponibles. Les bases de données valident les transactions. Personne ne clique dix-sept fois sur le même bouton parce que « rien n'avait l'air de s'être passé ».
La réalité est beaucoup moins coopérative. Les serveurs redémarrent. Les bases de données se bloquent mutuellement. Les réseaux disparaissent. Les fournisseurs infonuagiques ont des mauvaises journées. Quelqu'un déploie accidentellement un vendredi après-midi.
Parfois, c'est même vous.
La bonne nouvelle, c'est que nous nous préparons accidentellement à cela depuis le début de toute la série. Quand nous avons décidé de valider les webhooks avant de les traiter... Quand nous avons créé une table de boîte de réception au lieu de traiter les événements immédiatement... Quand nous avons séparé Slack de notre propre modèle de domaine...
... nous ne faisions pas qu'organiser du code. Nous bâtissions un système capable de se remettre d'une défaillance. Faire fonctionner quelque chose est un problème d'implémentation. Le faire continuer de fonctionner est un problème d'architecture.
Aujourd'hui, nous allons nous concentrer sur le deuxième.
L'échec est une fonctionnalité
Imaginez cette séquence d'événements :
Customer
│
▼
Slack Event
│
▼
Webhook Received
│
▼
Event Processor
│
▼
Database UpdateCela semble simple. Rejouons maintenant exactement la même séquence.
Customer
│
▼
Slack Event
│
▼
Webhook Received
│
▼
Event Processor
│
✖
Database DeadlockRien dans votre code n'a changé. Le client a toujours saisi le même message. Slack a toujours livré le même événement. La seule différence, c'est qu'un composant au milieu a eu une mauvaise journée.
La vraie question n'est pas si des défaillances se produisent. Elles vont se produire. La question est de savoir ce que votre application fait ensuite. Le message disparaît-il à jamais? Slack réessaie-t-il? Votre processeur réessaie-t-il? Quelqu'un reçoit-il deux copies? Quelqu'un doit-il corriger manuellement la base de données? Ou votre application se rétablit-elle discrètement sans que personne ne s'en aperçoive?
C'est ce qui distingue une démo d'un système de production.
Réussir est facile
Les développeurs aiment tester les requêtes réussies... « Le scénario idéal. » On remplit le formulaire correctement. On clique sur Soumettre. Tout fonctionne. On acquiesce d'un air approbateur et on passe à autre chose.
Tester les échecs exige une mentalité complètement différente. Au lieu de demander : « La fonctionnalité a-t-elle fonctionné? », commencez à poser des questions comme :
- Que se passe-t-il si la base de données est hors ligne?
- Que se passe-t-il si Slack livre cet événement deux fois?
- Que se passe-t-il si mon serveur plante à mi-chemin du traitement?
- Que se passe-t-il si la tâche planifiée cesse de s'exécuter pendant la nuit?
- Que se passe-t-il si mon API expire après que j'ai déjà sauvegardé la moitié du travail?
- Que se passe-t-il si je déploie une nouvelle version alors que des événements sont encore en file d'attente?
Ces questions révèlent généralement des faiblesses d'architecture bien avant vos clients. L'une de mes citations préférées est celle-ci :
Toute application est parfaitement conçue pour les défaillances qu'elle finit par subir.
Si vous n'avez jamais envisagé un mode de défaillance particulier, il y a de fortes chances que votre application ne le gère pas particulièrement bien.
Pourquoi les files d'attente changent tout
Plus tôt dans la série, nous avons présenté la boîte de réception des événements Slack. À l'époque, cela semblait être un endroit pratique pour stocker temporairement des charges utiles de webhook. Maintenant, cela devient quelque chose de beaucoup plus précieux. Cela devient un point de récupération. Imaginez que nous traitions chaque événement Slack directement dans le webhook.
Slack
│
▼
Webhook
│
▼
Business LogicSi votre serveur plante à mi-chemin du traitement, vous vous trouvez dans une position délicate. La base de données a-t-elle validé la transaction? Slack a-t-il reçu une réponse? Slack réessaiera-t-il? Avez-vous partiellement mis à jour la conversation?
Vous ne le savez peut-être pas vraiment. Comparons maintenant cela à notre architecture.
Slack
│
▼
Webhook
│
▼
Inbox Table
│
▼
Background ProcessorLe rôle du webhook est simplement d'enregistrer l'événement de façon sécuritaire. Une fois que la ligne existe dans la table de boîte de réception, nous avons déjà gagné. Même si le serveur prend immédiatement feu, l'événement n'a pas été perdu. (Si le serveur prend littéralement feu, vous avez des préoccupations plus importantes que votre clavardage de soutien, mais restons concentrés.)
Le processeur peut reprendre plus tard. Un autre serveur peut traiter la file d'attente. Une tâche planifiée peut le réessayer demain. L'événement attend patiemment jusqu'à ce que quelqu'un le traite avec succès.
C'est une propriété remarquablement puissante pour ce qui, au départ, ressemblait à une simple table de staging.
Le traitement doit être idempotent
Un mot revient régulièrement dans les discussions sur l'architecture orientée événements. Idempotent.
C'est aussi l'un de ces mots que les développeurs prétendent immédiatement avoir compris avant d'ouvrir discrètement un autre onglet du navigateur. Heureusement, le concept est beaucoup plus simple que l'orthographe. Une opération est idempotente si l'exécuter deux fois produit le même résultat que l'exécuter une fois.
Supposons que Slack livre cet événement :
{
"thread_ts": "1751032712.003900",
"ts": "1751032894.001200",
"text": "Can you try clearing your browser cache?"
}Votre processeur enregistre la réponse du soutien. Quelques instants plus tard, Slack décide que votre accusé de réception a pris un peu trop de temps et envoie exactement le même événement de nouveau. Sans idempotence, votre client voit maintenant ceci.
Support
Pouvez-vous essayer de vider le cache de votre navigateur ?
Pouvez-vous essayer de vider le cache de votre navigateur ?Ce n’est pas un problème de Slack. C’est un problème d’application. Heureusement, nous avons déjà tout ce qu’il faut. Slack attribue à chaque message un horodatage unique. Plus tôt, nous avons choisi de conserver cet horodatage avec la réponse du soutien.
Maintenant, nous posons simplement une question avant d’insérer quoi que ce soit.
if ( supportMessageService.existsBySlackTs( event.ts ) ) {
return;
}Si nous avons déjà traité ce message Slack, c’est terminé. Aucune exception, aucun avertissement, aucune réponse en double. Le processeur passe simplement à l’événement suivant. Une petite clause de garde élimine toute une catégorie de bogues de production.
C’est pourquoi l’idempotence est l’une des habitudes les plus précieuses que vous puissiez développer lorsque vous construisez des intégrations. N’assumez jamais qu’un événement arrivera une seule fois. Supposez qu’il arrivera éventuellement deux fois. Ou trois fois. Ou pendant un déploiement. Votre code devrait être tout aussi satisfait dans les deux cas.
Les échecs ne doivent pas tous être réessayés
Une fois que vous acceptez que des échecs se produisent, l’instinct naturel est de tout réessayer. Ne le faites pas. Certains échecs sont temporaires. D’autres ne le sont pas.
Un verrouillage temporaire de la base de données ? Réessayez.
Un délai d’attente réseau transitoire en communiquant avec Slack ? Réessayez.
Votre SQL Server redémarre après l’application d’un correctif ? Réessayez.
Mais qu’en est-il de ceci ?
var conversation = supportConversationService.getBySlackThread( event.thread_ts );Si la conversation n’existe pas parce que l’horodatage du fil est invalide, réessayer ce code encore cinquante fois ne fera pas apparaître soudainement la conversation.
Vous avez appris quelque chose d’important. Ce n’est pas un échec temporaire. C’est une mauvaise donnée.
Comprendre la différence entre les échecs transitoires et les échecs permanents est l’une des responsabilités les plus importantes d’un processeur d’événements. L’astuce n’est pas de tout réessayer. L’astuce consiste à ne réessayer que les choses qui ont une chance raisonnable de réussir au prochain essai.
Les messages toxiques ne disparaissent pas
Éventuellement, vous allez rencontrer un événement qui ne peut tout simplement pas être traité. Pas parce que votre base de données est indisponible. Pas parce que Slack est hors ligne. Pas à cause d’un problème réseau transitoire.
L’événement lui-même est mauvais.
Peut-être que quelqu’un a supprimé manuellement une conversation qui devrait toujours exister. Peut-être qu’un bogue a généré un horodatage de fil invalide. Peut-être qu’un événement Slack inattendu a passé vos filtres après que Slack a introduit un nouveau sous-type de message.
Quelle qu’en soit la raison, le fait de réessayer ne réglera pas le problème. Supposons que votre processeur ressemble à ceci.
try {
slackEventProcessor.process( event );
}
catch ( any e ) {
retryLater( event );
}Si process() lance la même exception à chaque fois, vous venez de créer une boucle infinie.
Attempt 1
│
▼
Exception
│
Retry
│
▼
Attempt 2
│
▼
Exception
│
Retry
│
▼
Attempt 3Quelque part autour de la tentative 4 763, votre processeur commence discrètement à se demander pourquoi il n’a pas été installé dans une machine Pac Man. C’est ce qu’on appelle couramment un message toxique. C’est une donnée parfaitement valide à stocker. C’est une donnée parfaitement valide à examiner. Ce n’est pas une donnée qu’il faut continuer à réessayer indéfiniment.
La fatigue des reprises
Un des ajouts les plus simples que vous puissiez faire à une table de boîte de réception est un compteur de reprises. Chaque tentative de traitement incrémente le compteur.
UPDATE slack_event_inbox
SET retry_count = retry_count + 1
WHERE slack_event_id = ?Éventuellement, vous atteignez une limite. Peut-être que c’est trois tentatives, ou cinq, ou dix. Le nombre exact importe beaucoup moins que le fait d’en avoir un. Une fois la limite de reprises dépassée, cessez de prétendre que la prochaine tentative sera magiquement différente. Déplacez l’événement ailleurs. Marquez-le comme ayant échoué. Avertissez quelqu’un. Faites autre chose que de consommer des ressources sans fin.
Rencontrez la file d’attente des lettres mortes
L’un de mes modèles architecturaux préférés a un nom franchement terrible - la file d’attente des lettres mortes. On dirait un endroit où les courriels abandonnés vont réfléchir à leurs choix de vie. En réalité, c’est beaucoup plus simple que cela. C’est simplement un endroit où les messages vont après que vous avez décidé qu’ils ne réussiront pas automatiquement.
Slack Event
│
▼
Inbox
│
▼
Processor
│
┌───┴─────────────┐
│ │
▼ ▼
Success Retry Needed
│
Retry Count Exceeded
│
▼
Dead-Letter QueueRemarquez ce qui ne s’est pas produit. L’événement n’a pas été supprimé. Supprimer les événements ayant échoué vous enlève la possibilité de les examiner plus tard.
Au lieu de cela, nous les avons mis en quarantaine. Ils ne bloquent plus le traitement normal, mais ils sont toujours disponibles pour inspection. Voyez la file d’attente des lettres mortes comme le dossier « Besoin de supervision d’un adulte ».
Votre file d’attente des lettres mortes n’a pas besoin de RabbitMQ
Si vous avez passé beaucoup de temps à lire sur les systèmes axés sur les événements, vous finirez par rencontrer Kafka, ou RabbitMQ, ou Azure Service Bus, ou Amazon SQS. Ce sont tous d’excellents outils. Ils sont aussi complètement inutiles pour de nombreuses applications ColdFusion. Votre file d’attente des lettres mortes peut tout simplement être une autre table de base de données.
slack_event_inbox
│
▼
slack_event_deadletterParfois, la solution la plus plate est la meilleure solution. Votre application sait déjà interroger une base de données. Vos administrateurs savent déjà examiner des lignes de base de données. Votre stratégie de sauvegarde inclut déjà votre base de données. Il n’y a rien de mal à utiliser l’infrastructure que vous avez déjà.
Si vous finissez par la dépasser, migrer vers un courtier de messages dédié est beaucoup plus facile que d’en introduire un avant d’en avoir réellement besoin.
La journalisation pour les humains
Les développeurs confondent souvent la journalisation avec le fait de vider les traces de pile dans un fichier. Ce sont deux choses différentes. Imaginez que vous ouvrez votre fichier journal à 2 h 13 du matin parce que le soutien vient de signaler que les réponses des clients ne s'affichent pas. Lequel de ces éléments aide le plus?
Null Pointer ExceptionOu ceci?
Slack Event Processing Failed
Event ID: 64381
Conversation: 1829
Slack Thread: 1751032712.003900
Slack User: U094832
Attempt: 3 of 5
Error: Conversation not found.L'un vous dit qu'une erreur s'est produite. L'autre vous indique où commencer à chercher. De bons journaux répondent à des questions. De mauvais journaux en créent de nouvelles.
Consignez le contexte, pas seulement les exceptions
Dans la mesure du possible, incluez suffisamment de contexte pour que quelqu'un puisse reproduire le problème sans ouvrir un débogueur. Des éléments comme :
- Slack Event ID
- Thread Timestamp
- Conversation ID
- Slack User ID
- Retry Count
- Processing Duration
- Exception Message
Remarquez ce qui ne figure pas dans cette liste. L'intégralité de la charge utile Slack. Journaliser chaque charge utile entrante pourrait sembler une bonne idée jusqu'à ce que quelqu'un envoie une pièce jointe de dix mégaoctets ou que vos fichiers journaux exigent soudainement leur propre SAN.
Consignez les informations que vous utiliserez réellement. Tout le reste existe déjà dans votre table de boîte de réception.
Créez votre propre page d'opérations
Plus tôt dans la série, nous avons créé des pages pour les clients. Ensuite, des pages pour les représentants du soutien. Maintenant, il est temps de créer une page pour vous-même.
Elle n'a pas besoin d'être jolie. Elle n'a même pas besoin de Bootstrap. Même si, il faut l'admettre, tout est plus beau avec Bootstrap.
Imaginez ouvrir un tableau de bord interne et voir immédiatement ceci.
Slack Event Inbox
Pending: 12
Processing: 1
Failed: 2
Oldest Pending Event: 18 seconds
Oldest Failed Event: 2 hours
Retries Today: 31Cette petite page vous en dit plus sur l'état de votre intégration qu'un millier de requêtes webhook réussies. La visibilité opérationnelle n'est pas quelque chose qu'on ajoute à une application après son lancement. Elle fait partie de l'application. Parce qu'un jour ou l'autre, quelqu'un va demander : « Le chat de soutien est-il brisé? »
Il est beaucoup plus agréable de pouvoir répondre à cette question avec des données qu'avec : « Il semble correct sur ma machine. »
Parfois, la file d'attente est le canari
L'un des effets secondaires intéressants des architectures fondées sur les files d'attente est que la file elle-même devient un système d'alerte précoce.
Supposez que votre application traite normalement les événements Slack en moins d'une seconde.
Soudainement, votre tableau de bord affiche ceci.
Pending Events
9:30 0
9:35 1
9:40 3
9:45 17
9:50 61La file n'est pas le problème. Elle vous indique qu'il y a un problème ailleurs. Peut-être que SQL Server est très sollicité, ou que Slack répond lentement. Peut-être que quelqu'un a accidentellement déployé une boucle infinie. Quelle qu'en soit la cause, votre file vous donne une indication précoce que quelque chose en aval a cessé de suivre le rythme.
Le bon côté, c'est que les clients ne l'ont probablement pas encore remarqué. Vous avez encore le temps de régler le problème avant que le soutien commence à recevoir des courriels qui commencent par : « Je me demandais juste si vous aviez reçu mon message... »
Se remettre des pannes
L'un des effets secondaires intéressants d'une conception pensée pour l'échec est que la reprise devient souvent étonnamment simple. Imaginez que votre application se mette hors ligne pendant vingt minutes pendant que vous déployez une nouvelle version. Pendant ce temps, les clients continuent d'envoyer des demandes de soutien. Les représentants du soutien continuent de répondre dans Slack. Slack continue de livrer des événements webhook. Votre serveur Web n'est pas là pour les recevoir.
Que se passe-t-il?
J'espère que Slack réessaie. Si c'est le cas, parfait. Sinon, vous avez toujours un problème.
Imaginez maintenant une autre panne. Votre webhook fonctionne toujours. La table de boîte de réception continue de recevoir des événements. Mais votre tâche planifiée qui traite ces événements s'est accidentellement arrêtée pendant la nuit. Votre tableau de bord ressemble maintenant à ceci :
Pending Events
8:00 0
8:15 3
8:30 12
8:45 48
9:00 113Ce n'est pas joli, mais c'est récupérable. La tâche planifiée recommence à s'exécuter. Elle commence à traiter les événements. La file rétrécit. Éventuellement, tout se remet à niveau. Personne ne copie manuellement des charges utiles JSON, ne demande à Slack de renvoyer quoi que ce soit ni n'édite les lignes de la base de données à la main. Votre application reprend simplement là où elle s'était arrêtée.
C'est l'un des plus grands avantages des systèmes fondés sur les files d'attente. Ils peuvent naturellement reprendre.
La reprise devrait être ennuyeuse
Quand la production a un problème, l'excitation est généralement mauvais signe. La meilleure procédure de reprise ressemble généralement à ceci.
Problem Found
│
▼
Problem Fixed
│
▼
Restart Processor
│
▼
Queue Drains
│
▼
Everything Returns to NormalC'est tout. Pas de mises à jour SQL. Pas de scripts d'urgence. Pas de copie de données entre les tables. Si votre architecture exige des prouesses chaque fois qu'un problème survient, elle mérite probablement un autre regard.
L'objectif n'est pas d'éliminer les pannes, c'est de rendre la reprise routinière.
Tout assembler
Prenons un pas de recul et regardons le portrait complet.
Slack
│
▼
Verified Webhook
│
▼
Event Inbox Table
│
▼
slack_event_processor.cfc
│
┌──────────────┴──────────────┐
│ │
▼ ▼
Transient Failure Permanent Failure
│ │
▼ ▼
Retry Later Dead-Letter Queue
│
▼
Success
│
▼
Persist Support Reply
│
▼
Publish SSE Event
│
▼
Customer BrowserL'application ne dépend jamais du fait que tout fonctionne parfaitement. Chaque étape suppose que l'étape précédente pourrait échouer à l'occasion. C'est ce qui rend le pipeline résilient. Pas parce que les échecs n'arrivent jamais, mais parce qu'ils étaient attendus.
Ce que nous avons construit
Au cours des huit derniers articles, nous sommes passés d'un simple point de terminaison webhook à un pipeline de messagerie prêt pour la production.
- Les clients peuvent entamer des conversations sans jamais penser à Slack.
- Les représentants du soutien peuvent répondre sans jamais ouvrir votre application.
- Les messages sont vérifiés avant qu'on leur fasse confiance.
- Les événements sont mis en file d'attente avant d'être traités.
- Les réponses deviennent des objets de domaine de première classe.
- Les navigateurs se mettent à jour en temps réel.
- Les livraisons en double ne créent pas de messages en double.
- Les échecs temporaires sont relancés.
- Les échecs permanents sont mis en quarantaine.
- L'équipe des opérations a une visibilité sur ce qui se passe.
Peut-être plus important encore, le système ne panique pas quand quelque chose tourne mal. Il continue simplement de fonctionner. Ou attend patiemment jusqu'à ce qu'il le puisse.
Les plus grandes leçons
S'il y a une idée que j'espère voir rester après cet article, c'est celle-ci : un logiciel de production ne se définit pas par son comportement quand tout fonctionne. Il se définit par son comportement quand tout ne fonctionne pas.
Tout au long du parcours, nous avons renforcé plusieurs idées qui s'appliquent bien au-delà des intégrations Slack.
- Séparer le travail de réception du travail de traitement.
- Faire en sorte que chaque opération puisse être exécutée plus d'une fois sans danger.
- Réessayer les échecs transitoires.
- Cesser de réessayer les échecs permanents.
- Ne jamais supprimer des données que vous n'avez pas encore examinées.
- Créer des outils opérationnels avant d'en avoir besoin.
- Concevoir des procédures de reprise qui sont ennuyeuses.
Aucune de ces idées n'est particulièrement excitante. C'est précisément pour cette raison qu'elles fonctionnent. Les systèmes fiables sont habituellement construits à partir d'une foule de petites décisions ennuyeuses plutôt que d'une brillante percée architecturale.
Retour en arrière
Quand nous avons commencé cette série, notre objectif semblait trompeusement simple. Construire un clavardage d'assistance. Quelque part en cours de route, nous avons fini par discuter des signatures webhook, du traitement asynchrone, des travailleurs en arrière-plan, de la modélisation de domaine, de Server-Sent Events, de l'idempotence, des stratégies de nouvelle tentative, des files d'attente de lettres mortes et de la visibilité opérationnelle.
C'est parce que le clavardage d'assistance n'a jamais vraiment été le point central. Le clavardage d'assistance n'était que le véhicule. Le véritable objectif était d'apprendre à construire des intégrations qui se comportent comme des citoyens de première classe à l'intérieur de votre application plutôt que comme une collection de requêtes HTTP assemblées avec de l'espoir. Ces leçons se transfèrent remarquablement bien.
L'intégration Slack d'aujourd'hui pourrait facilement devenir l'intégration Microsoft Teams de demain, ou un processeur de webhook Stripe, ou une application GitHub, ou un moteur de synchronisation Salesforce.
Les technologies changeront. Les principes architecturaux, eux, ne changeront pas.
Dans le dernier article
Notre intégration Slack est maintenant fiable. Les messages survivent aux échecs. Les réponses se rétablissent gracieusement. L'équipe des opérations dispose des outils nécessaires pour comprendre ce qui se passe.
À ce stade, nous avons construit toutes les pièces importantes. Le problème, c'est qu'elles sont actuellement réparties sur huit articles, plusieurs composants, une poignée de tables de base de données et suffisamment d'extraits de code pour que l'assemblage de l'ensemble ressemble à une expédition archéologique.
Alors, pour le dernier article de cette série, nous allons tout rassembler.
Nous construirons un squelette complet et autonome de clavardage d'assistance Page-to-Slack et nous le publierons sous forme de dépôt téléchargeable. Il comprendra la page de clavardage destinée aux clients, l'intégration à l'API Slack, la vérification sécurisée des événements, la boîte de réception des événements, le traitement en arrière-plan, le mappage des conversations, les réponses du soutien, Server-Sent Events, la gestion des nouvelles tentatives et le schéma de base de données nécessaire pour que tout fonctionne.
L'objectif ne sera pas de créer une plateforme d'assistance achevée pour toutes les applications possibles. Il s'agira de fournir un point de départ propre et compréhensible que vous pourrez télécharger, configurer avec vos propres identifiants Slack et votre propre canal, puis exécuter.
Vous devrez tout de même adapter l'authentification, le style, le modèle de données et les détails de déploiement pour convenir à votre application. Mais vous n'aurez pas à reconstruire l'architecture à partir de huit articles distincts en vous demandant quel extrait de code précéda telle migration de base de données.
À la fin, vous aurez un squelette fonctionnel de bout en bout :
Customer Page
│
▼
ColdFusion Application
│
▼
Slack Support Thread
│
▼
Support Reply
│
▼
Customer BrowserAjoutez vos clés. Choisissez votre canal. Exécutez le SQL. Démarrez l'application. Puis faites-en la vôtre. Parce qu'après huit articles sur le fonctionnement des différentes pièces, il est temps de vous remettre toutes les pièces dans une seule boîte.
