Le bot a commencé à renvoyer la même réponse deux ou trois fois chaque fois qu'un utilisateur cliquait frénétiquement sur le bouton d'envoi. La duplication n'apparaissait que pour les personnes tapant assez vite pour envoyer plusieurs messages avant que l'IA ne commence à réfléchir, et elle est restée cachée en production pendant longtemps car le schéma était rare. Un verrou de base de données prématuré – libéré quelques millisecondes après avoir été acquis – laissait la conversation sans protection, permettant à plusieurs processus de répondre au même prompt.

Pourquoi le verrou a échoué

Le code acquérait un verrou lors d'un seul appel à la base de données, puis rendait immédiatement le contrôle au gestionnaire de requête. La durée de vie du verrou se mesurait en millisecondes, bien plus courte que le temps nécessaire au modèle d'IA pour générer une réponse. Au moment où le modèle commençait son travail, le verrou avait déjà disparu, rien n'empêchant donc une seconde requête de saisir le même enregistrement de conversation et de générer une autre réponse.

Deux symptômes sont apparus :

  • Des réponses identiques étaient envoyées à la suite.
  • Des réponses légèrement reformulées apparaissaient pour la même question, car chaque processus construisait son propre prompt à partir de la même saisie utilisateur.

Comme la plupart des utilisateurs font une pause entre les messages, le bug est passé inaperçu. Seuls les utilisateurs tapant rapidement déclenchaient la condition de concurrence, et les cas étaient rares.

La solution de fortune qui n'a pas suffi

La première réaction a été d'ajouter un court délai après l'arrivée d'un message, dans l'espoir de créer un effet d'anti-rebond (debounce) sur les saisies rapides. Cela aidait lorsque deux messages arrivaient en succession rapide, mais le système s'effondrait si un troisième message apparaissait alors que l'IA générait encore du texte.

Un second problème est apparu lorsque les minuteurs et les données de conversation résidaient dans le même compartiment de stockage. Lorsque le bot finissait de traiter une requête, il écrasait l'enregistrement du minuteur, supprimant ainsi son propre compte à rebours. Le système perdait la trace des messages déjà répondus, ouvrant la porte à de nouvelles duplications.

Construire une protection fiable : compteurs de version, minuteurs isolés et bail (lease)

L'équipe a repensé le flux autour de trois piliers :

  • Compteur de version – chaque message entrant incrémente un compteur stocké avec la conversation. Le compteur indique au système combien de messages sont arrivés depuis la dernière réponse, ce qui permet de détecter facilement de nouvelles saisies pendant qu'une réponse est en cours de génération.
  • Fenêtre d'anti-rebond dédiée – les minuteurs résident désormais dans une zone de stockage séparée, isolée des données de conversation. Un plafond strict sur la durée de l'anti-rebond empêche un utilisateur de bloquer le bot indéfiniment.
  • Bail de session (lease) – le verrou original est remplacé par un bail qui porte un horodatage d'expiration explicite. Le bail est acquis via une opération compare-and-swap (CAS) : le processus lit la valeur actuelle du bail, n'écrit une nouvelle valeur que si l'ancienne correspond, et obtient ainsi l'exclusivité sur la conversation. Si le processus plante, le bail expire automatiquement, libérant la conversation pour le gestionnaire suivant.

Comment fonctionne le nouveau pipeline

  1. Arrivée du message – le système incrémente le compteur de version et (ré)initialise le minuteur d'anti-rebond. Il répond immédiatement au client, sans lancer l'IA.
  2. Expiration du minuteur – le gestionnaire de minuteur tente d'acquérir le bail. Si le CAS réussit, le gestionnaire poursuit ; sinon, il recule, sachant qu'un autre processus possède déjà la conversation.
  3. Vérification de nouvelles entrées – le gestionnaire compare le compteur de version actuel avec la valeur enregistrée au début du minuteur. Si le compteur a avancé, il regroupe les messages en attente en un seul prompt.
  4. Génération d'une réponse – le modèle d'IA s'exécute une seule fois, produisant une réponse unique qui couvre toutes les saisies utilisateur récentes.
  5. Dernière vérification de cohérence – juste avant l'envoi de la réponse, le gestionnaire relit le compteur de version. Si un message plus récent est arrivé pendant la génération, la réponse est rejetée et le processus redémarre le minuteur, garantissant qu'aucune réponse obsolète n'atteigne l'utilisateur.

Cette approche élimine les réponses en double, limite le temps pendant lequel une conversation peut être bloquée et se rétablit automatiquement en cas de plantage de processus grâce à l'expiration automatique du bail.

À retenir

Un verrou qui disparaît avant le début de la section critique n'offre aucune protection. En remplaçant un verrou de base de données éphémère par un bail explicite avec expiration et en isolant les minuteurs des données de conversation, le bot garantit désormais une réponse unique et à jour, même lorsque les utilisateurs tapent à la vitesse de l'éclair. Cet épisode souligne une leçon intemporelle : les mécanismes de protection de la concurrence doivent durer plus longtemps que le travail qu'ils protègent, sinon ils deviennent des barrières invisibles laissant passer les bugs.