بات هر زمان که کاربر دکمه ارسال را با سرعت و مکرر فشار می‌داد، شروع به ارسال همان پاسخ به صورت دو یا سه بار می‌کرد. این تکرار فقط برای کاربرانی رخ می‌داد که آن‌قدر سریع تایپ می‌کردند که پیش از شروع فرآیند تفکر هوش مصنوعی، چندین پیام ارسال می‌شد؛ و چون این الگو نادر بود، برای مدت طولانی در محیط عملیاتی (production) پنهان ماند. یک قفل پایگاه داده زودرس – که تنها چند میلی‌ثانیه پس از گرفته شدن آزاد می‌شد – باعث می‌شد گفتگو بدون محافظ باقی بماند و به چندین فرآیند اجازه دهد تا به یک پرامپت واحد پاسخ دهند.

چرا قفل با شکست مواجه شد

کد، قفل را در یک فراخوانی واحد پایگاه داده به‌دست می‌آورد و بلافاصله کنترل را به مدیریت‌کننده درخواست (request handler) بازمی‌گرداند. طول عمر این قفل بر حسب میلی‌ثانیه بود، که بسیار کوتاه‌تر از زمانی است که مدل هوش مصنوعی برای تولید پاسخ نیاز دارد. زمانی که مدل کار خود را شروع می‌کرد، قفل از قبل از بین رفته بود؛ بنابراین هیچ چیزی مانع از آن نمی‌شد که درخواست دوم، همان رکورد گفتگو را تصاحب کرده و پاسخ دیگری صادر کند.

دو علامت (symptom) از این مشکل پدیدار شد:

  • پاسخ‌های کاملاً یکسان، پشت سر هم ارسال می‌شدند.
  • پاسخ‌هایی با عبارت‌بندی‌های کمی متفاوت برای یک سوال مشابه ظاهر می‌شدند، زیرا هر فرآیند پرامپت خود را بر اساس همان ورودی کاربر می‌ساخت.

از آنجایی که اکثر کاربران بین پیام‌ها مکث می‌کنند، این باگ دور از چشم باقی ماند. تنها تایپیست‌های سریع باعث ایجاد شرایط رقابتی (race condition) می‌شدند و این موارد نیز نادر بودند.

راهکار نیم‌بند که کارساز نبود

اولین واکنش، اضافه کردن یک تأخیر کوتاه پس از رسیدن پیام بود، با این امید که ورودی‌های سریع را «دی‌بانس» (debounce) کند. این کار زمانی که دو پیام با فاصله بسیار کم می‌رسیدند کمک می‌کرد، اما اگر پیام سومی در حالی که هوش مصنوعی هنوز در حال تولید متن بود ظاهر می‌شد، راهکار از هم می‌پاشید.

مشکل دوم زمانی آشکار شد که تایمرها و داده‌های گفتگو در یک مخزن ذخیره‌سازی مشترک قرار داشتند. وقتی بات پردازش یک درخواست را تمام می‌کرد، رکورد تایمر را بازنویسی می‌کرد و در واقع شمارش معکوس خودش را پاک می‌نمود. سیستم کنترل خود را روی اینکه کدام پیام‌ها قبلاً پاسخ داده شده‌اند از دست می‌داد و این امر راه را برای تکرارهای بیشتر باز می‌کرد.

ساخت یک محافظ قابل اعتماد: شمارنده‌های نسخه، تایمرهای مجزا و یک lease

تیم، جریان کار را بر پایه سه ستون اصلی بازطراحی کرد:

  • شمارنده نسخه (Version counter) – هر پیام ورودی، شمارنده‌ای را که همراه با گفتگو ذخیره شده است، افزایش می‌دهد. این شمارنده به سیستم می‌گوید از آخرین پاسخ، چند پیام رسیده است و تشخیص ورودی‌های جدید در حین تولید پاسخ را آسان می‌کند.
  • پنجره دی‌بانس اختصاصی (Dedicated debounce window) – تایمرها اکنون در یک ناحیه ذخیره‌سازی مجزا قرار دارند و از بار داده‌های (payload) گفتگو ایزوله شده‌اند. تعیین یک سقف مشخص برای مدت زمان دی‌بانس از این جلوگیری می‌کند که کاربر بتواند بات را برای مدت نامحدود معطل نگه دارد.
  • اجاره نشست (Session lease) – قفل اصلی با یک lease جایگزین شده است که دارای یک برچسب زمانی انقضای صریح است. این lease با استفاده از عملیات مقایسه و جایگزینی (CAS) تصاحب می‌شود: فرآیند مقدار فعلی lease را می‌خواند و تنها در صورتی مقدار جدید را می‌نویسد که مقدار قدیمی مطابقت داشته باشد؛ و از این طریق حق انحصاری گفتگو را به‌دست می‌آورد. اگر فرآیند کرش کند، lease به‌طور خودکار منقضی شده و گفتگو را برای مدیریت‌کننده بعدی آزاد می‌کند.

نحوه عملکرد خط لوله (pipeline) جدید

  1. رسیدن پیام – سیستم شمارنده نسخه را افزایش داده و تایمر دی‌بانس را (دوباره) تنظیم می‌کند. سیستم بلافاصله بدون شروع کار هوش مصنوعی، پاسخ را به کلاینت برمی‌گرداند.
  2. انقضای تایمر – مدیریت‌کننده تایمر تلاش می‌کند تا lease را به‌دست آورد. اگر عملیات CAS با موفقیت انجام شود، مدیریت‌کننده ادامه می‌دهد؛ در غیر این صورت، با دانستن اینکه فرآیند دیگری در حال حاضر مالک گفتگو است، عقب‌نشینی می‌کند.
  3. بررسی ورودی جدید – مدیریت‌کننده، شمارنده نسخه فعلی را با مقداری که هنگام شروع تایمر ثبت کرده بود، مقایسه می‌کند. اگر شمارنده افزایش یافته باشد، پیام‌های معلق را در قالب یک پرامپت واحد تجمیع می‌کند.
  4. تولید پاسخ – مدل هوش مصنوعی تنها یک بار اجرا می‌شود و یک پاسخ واحد تولید می‌کند که تمام ورودی‌های اخیر کاربر را پوشش می‌دهد.
  5. بررسی صحت نهایی (Final sanity check) – درست قبل از ارسال پاسخ، مدیریت‌کننده دوباره شمارنده نسخه را می‌خواند. اگر پیام جدیدتری در حین تولید پاسخ رسیده باشد، پاسخ فعلی دور ریخته شده و فرآیند دوباره تایمر را شروع می‌کند تا اطمینان حاصل شود که هیچ پاسخ قدیمی و بی‌ربطی به کاربر نمی‌رسد.

این رویکرد پاسخ‌های تکراری را حذف می‌کند، زمان معطل ماندن گفتگو را محدود می‌کند و به دلیل انقضای خودکار lease، به‌طور خودکار از کرش کردن فرآیندها بازیابی می‌شود.

نتیجه‌گیری

قفل‌ای که پیش از شروع بخش بحرانی (critical section) از بین می‌رود، هیچ حفاظتی ایجاد نمی‌کند. با جایگزینی یک قفل گذرای پایگاه داده با یک lease صریح و منقضی‌شونده و ایزوله کردن تایمرها از داده‌های گفتگو، بات اکنون تضمین می‌کند که حتی زمانی که کاربران با سرعت برق و باد تایپ می‌کنند، یک پاسخ واحد و به‌روز دریافت کنند. این تجربه یک درس همیشگی را یادآوری می‌کند: محافظ‌های همزمانی (concurrency safeguards) باید بیشتر از کاری که از آن محافظت می‌کنند دوام بیاورند، در غیر این صورت به موانع نامرئی تبدیل می‌شوند که اجازه می‌دهند باگ‌ها از میان آن‌ها عبور کنند.