Der Bot gab immer wieder zwei- oder dreimal dieselbe Antwort aus, wann immer ein Benutzer wild auf den Senden-Button hämmerte. Die Duplikate traten nur bei Personen auf, die schnell genug tippten, um mehrere Nachrichten abzufeuern, bevor die KI mit dem Nachdenken begann, und da das Muster selten war, blieb der Fehler in der Produktion lange unentdeckt. Eine vorzeitige Datenbank-Sperre (Lock) – die nur wenige Millisekunden nach ihrer Aktivierung wieder freigegeben wurde – ließ die Konversation ungeschützt, sodass mehrere Prozesse dieselbe Anfrage beantworten konnten.

Warum der Lock fehlschlug

Der Code erwarb einen Lock in einem einzigen Datenbankaufruf und übergab die Kontrolle dann sofort wieder an den Request-Handler. Die Lebensdauer des Locks wurde in Millisekunden gemessen, was weitaus kürzer war als die Zeit, die das KI-Modell benötigt, um eine Antwort zu generieren. Zu dem Zeitpunkt, als das Modell mit der Arbeit begann, war der Lock bereits verschwunden, sodass nichts verhinderte, dass eine zweite Anfrage denselben Konversationsdatensatz erfasste und eine weitere Antwort auslöste.

Zwei Symptome traten auf:

  • Identische Antworten wurden unmittelbar hintereinander gesendet.
  • Leicht umformulierte Antworten erschienen zur selben Frage, da jeder Prozess seinen eigenen Prompt aus derselben Benutzereingabe erstellte.

Da die meisten Benutzer zwischen den Nachrichten pausieren, blieb der Bug unter dem Radar. Nur sehr schnelle Tipper lösten die Race Condition aus, und auch diese Fälle waren selten.

Der halbherzige Fix, der nicht ausreichte

Die erste Reaktion war, eine kurze Verzögerung nach dem Eintreffen einer Nachricht einzubauen, in der Hoffnung, die schnellen Eingaben zu „entprellen“ (Debounce). Das half, wenn zwei Nachrichten in schneller Folge eintrafen, versagte jedoch, wenn eine dritte Nachricht auftauchte, während die KI noch Text generierte.

Ein zweites Problem trat auf, als Timer und Konversationsdaten im selben Speicherbereich (Storage Bucket) lagen. Wenn der Bot die Verarbeitung einer Anfrage abschloss, überschrieb er den Timer-Datensatz und löschte damit effektiv seinen eigenen Countdown. Das System verlor den Überblick darüber, welche Nachrichten bereits beantwortet worden waren, was Tür und Tor für weitere Duplikate öffnete.

Aufbau eines zuverlässigen Schutzes: Versionszähler, isolierte Timer und ein Lease

Das Team gestaltete den Ablauf basierend auf drei Säulen neu:

  • Versionszähler – jede eingehende Nachricht erhöht einen mit der Konversation gespeicherten Zähler. Der Zähler teilt dem System mit, wie viele Nachrichten seit der letzten Antwort eingegangen sind, wodurch neue Eingaben während der Generierung einer Antwort leicht erkannt werden können.
  • Dedizierter Debounce-Zeitraum – Timer befinden sich nun in einem separaten Speicherbereich, isoliert von den Konversationsdaten. Eine feste Obergrenze für die Debounce-Dauer verhindert, dass ein Benutzer den Bot unendlich lange blockiert.
  • Session-Lease – der ursprüngliche Lock wird durch einen Lease ersetzt, der einen expliziten Ablaufzeitstempel trägt. Der Lease wird mittels einer Compare-and-Swap (CAS)-Operation beansprucht: Der Prozess liest den aktuellen Lease-Wert, schreibt nur dann einen neuen, wenn der alte Wert übereinstimmt, und erlangt so exklusive Rechte an der Konversation. Wenn der Prozess abstürzt, läuft der Lease automatisch ab und gibt die Konversation für den nächsten Handler frei.

So funktioniert die neue Pipeline

  1. Eingang der Nachricht – das System erhöht den Versionszähler und setzt den Debounce-Timer (neu) auf. Es antwortet dem Client sofort, ohne die KI zu starten.
  2. Ablauf des Timers – der Timer-Handler versucht, den Lease zu beanspruchen. Wenn das CAS erfolgreich ist, fährt der Handler fort; andernfalls zieht er sich zurück, da er weiß, dass ein anderer Prozess die Konversation bereits besitzt.
  3. Prüfung auf neue Eingaben – der Handler vergleicht den aktuellen Versionszähler mit dem Wert, den er beim Start des Timers aufgezeichnet hat. Wenn sich der Zähler erhöht hat, fasst er die ausstehenden Nachrichten zu einem einzigen Prompt zusammen.
  4. Generierung einer Antwort – das KI-Modell wird einmal ausgeführt und erzeugt eine einzige Antwort, die alle aktuellen Benutzereingaben abdeckt.
  5. Abschließender Sanity-Check – Kurz bevor die Antwort gesendet wird, liest der Handler den Versionszähler erneut aus. Wenn während der Generierung eine neuere Nachricht eingegangen ist, wird die Antwort verworfen und der Prozess startet den Timer neu, um sicherzustellen, dass keine veraltete Antwort den Benutzer erreicht.

Dieser Ansatz eliminiert doppelte Antworten, begrenzt die Zeit, in der eine Konversation blockiert werden kann, und erholt sich automatisch von Prozessabstürzen, da der Lease von selbst abläuft.

Fazit

Ein Lock, der verschwindet, bevor der kritische Abschnitt beginnt, bietet keinerlei Schutz. Durch den Ersatz eines flüchtigen Datenbank-Locks durch einen expliziten, zeitlich begrenzten Lease und die Isolierung der Timer von den Konversationsdaten garantiert der Bot nun eine einzige, aktuelle Antwort, selbst wenn Benutzer mit blitzartiger Geschwindigkeit tippen. Dieser Vorfall unterstreicht eine zeitlose Lektion: Schutzmaßnahmen für Nebenläufigkeit müssen länger halten als die Arbeit, die sie schützen, andernfalls werden sie zu unsichtbaren Barrieren, durch die Bugs hindurchschlüpfen können.