بدأ البوت في إرسال نفس الإجابة مرتين أو ثلاث مرات كلما ضغط المستخدم على زر الإرسال بشكل متكرر وسريع. ظهر التكرار فقط للأشخاص الذين يكتبون بسرعة كافية لإرسال عدة رسائل قبل أن يبدأ الذكاء الاصطناعي في التفكير، وظل الأمر مخفيًا في بيئة الإنتاج لفترة طويلة لأن هذا النمط كان نادر الحدوث. أدى قفل قاعدة بيانات مبكر - تم تحريره بعد بضعة أجزاء من الثانية من أخذه - إلى ترك المحادثة دون حماية، مما سمح لعمليات متعددة بالرد على نفس المطالبة.

لماذا فشل القفل

حصل الكود على قفل في استدعاء واحد لقاعدة البيانات، ثم أعاد التحكم فورًا إلى معالج الطلبات. كانت مدة حياة القفل تُقاس بالأجزاء من الثانية، وهي أقصر بكثير من الوقت الذي يحتاجه نموذج الذكاء الاصطناعي لإنشاء رد. وبحلول الوقت الذي بدأ فيه النموذج العمل، كان القفل قد اختفى بالفعل، لذا لم يمنع أي شيء الطلب الثاني من الحصول على سجل المحادثة نفسه وإصدار رد آخر.

ظهرت علامتان:

  • إرسال ردود متطابقة متتالية.
  • ظهور ردود تمت إعادة صياغتها قليلاً لنفس السؤال، حيث قامت كل عملية ببناء مطالبتها الخاصة من نفس مدخلات المستخدم.

نظرًا لأن معظم المستخدمين يتوقفون قليلاً بين الرسائل، ظل الخطأ بعيدًا عن الأنظار. فقط من يكتبون بسرعة هم من تسببوا في حدوث "حالة التسابق" (race condition)، وكانت هذه الحالات نادرة.

الإصلاح غير المكتمل الذي لم يجدِ نفعًا

كان الرد الأول هو إضافة تأخير قصير بعد وصول الرسالة، على أمل "تخفيف حدة" (debounce) المدخلات السريعة. ساعد ذلك عندما تصل رسالتان في تتابع سريع، لكن الأمر فشل إذا ظهرت رسالة ثالثة بينما كان الذكاء الاصطناعي لا يزال ينشئ النص.

ظهرت مشكلة ثانية عندما كانت المؤقتات وبيانات المحادثة موجودة في نفس وعاء التخزين. عندما ينتهي البوت من معالجة الطلب، فإنه يقوم بالكتابة فوق سجل المؤقت، مما يؤدي فعليًا إلى حذف العد التنازلي الخاص به. فقد النظام القدرة على تتبع الرسائل التي تمت الإجابة عليها بالفعل، مما فتح الباب لمزيد من التكرار.

بناء حماية موثوقة: عدادات الإصدار، والمؤقتات المعزولة، والتصريح المؤقت (Lease)

أعاد الفريق تصميم التدفق حول ثلاث ركائز:

  • عداد الإصدار (Version counter) – تزيد كل رسالة واردة عدادًا مخزنًا مع المحادثة. يخبر العداد النظام بعدد الرسائل التي وصلت منذ آخر رد، مما يسهل اكتشاف المدخلات الجديدة أثناء إنشاء الرد.
  • نافذة debounce مخصصة – تعيش المؤقتات الآن في منطقة تخزين منفصلة، معزولة عن حمولة بيانات المحادثة. يمنع الحد الأقصى لزمن الـ debounce المستخدم من تعطيل البوت إلى أجل غير مسمى.
  • تصريح الجلسة (Session lease) – تم استبدال القفل الأصلي بتصريح (lease) يحمل طابعًا زمنيًا صريحًا لانتهاء الصلاحية. يتم الحصول على التصريح باستخدام عملية المقارنة والتبديل (CAS): تقرأ العملية قيمة التصريح الحالية، وتكتب قيمة جديدة فقط إذا كانت القيمة القديمة مطابقة، وبذلك تكتسب حقوقًا حصرية للمحادثة. إذا تعطلت العملية، تنتهي صلاحية التصريح تلقائيًا، مما يحرر المحادثة للمعالج التالي.

كيف يعمل المسار الجديد

  1. وصول الرسالة – يزيد النظام عداد الإصدار ويضبط (أو يعيد ضبط) مؤقت الـ debounce. يعود النظام إلى العميل فورًا، دون بدء عملية الذكاء الاصطناعي.
  2. انتهاء صلاحية المؤقت – يحاول معالج المؤقت الحصول على التصريح. إذا نجحت عملية CAS، يستمر المعالج؛ وإلا فإنه يتراجع، مدركًا أن هناك عملية أخرى تمتلك المحادثة بالفعل.
  3. التحقق من المدخلات الجديدة – يقارن المعالج عداد الإصدار الحالي بالقيمة التي سجلها عند بدء المؤقت. إذا تقدم العداد، فإنه يجمع الرسائل المعلقة في مطالبة واحدة.
  4. إنشاء الرد – يعمل نموذج الذكاء الاصطناعي مرة واحدة، لينتج إجابة واحدة تغطي جميع مدخلات المستخدم الأخيرة.
  5. فحص نهائي للتأكد من الصحة – قبل إرسال الرد مباشرة، يقرأ المعالج عداد الإصدار مرة أخرى. إذا وصلت رسالة أحدث أثناء عملية الإنشاء، يتم تجاهل الرد ويعيد المعالج تشغيل المؤقت، مما يضمن عدم وصول أي إجابة قديمة إلى المستخدم.

يقضي هذا النهج على الردود المكررة، ويضع حدًا للوقت الذي يمكن أن تتعطل فيه المحادثة، ويتعافى تلقائيًا من تعطل العمليات لأن التصريح ينتهي تلقائيًا.

الخلاصة

إن القفل الذي يختفي قبل بدء القسم الحرج لا يوفر أي حماية على الإطلاق. من خلال استبدال قفل قاعدة البيانات العابر بتصريح صريح ومنتهي الصلاحية وعزل المؤقتات عن بيانات المحادثة، يضمن البوت الآن ردًا واحدًا ومحدثًا حتى عندما يكتب المستخدمون بسرعة البرق. تؤكد هذه الواقعة على درس خالد: يجب أن تدوم ضمانات التزامن لفترة أطول من العمل الذي تحميه، وإلا ستصبح حواجز غير مرئية تسمح بمرور الأخطاء.