जब भी कोई यूजर सेंड बटन को तेजी से दबाता था, बॉट एक ही जवाब दो या तीन बार देने लगता था। यह डुप्लिकेशन केवल उन लोगों के लिए दिखाई देता था जो AI के सोचने शुरू करने से पहले ही कई मैसेज भेज देने के लिए पर्याप्त तेजी से टाइप करते थे, और क्योंकि यह पैटर्न दुर्लभ था, इसलिए यह लंबे समय तक प्रोडक्शन में छिपा रहा। एक समय से पहले डेटाबेस लॉक — जो लेने के कुछ मिलीसेकंड बाद ही रिलीज हो गया था — ने बातचीत को असुरक्षित छोड़ दिया, जिससे कई प्रोसेस एक ही प्रॉम्प्ट का जवाब देने लगे।
लॉक क्यों विफल हुआ
कोड ने एक सिंगल डेटाबेस कॉल में लॉक प्राप्त किया, और फिर तुरंत कंट्रोल रिक्वेस्ट हैंडलर को वापस सौंप दिया। लॉक की अवधि मिलीसेकंड में मापी गई थी, जो AI मॉडल को रिस्पॉन्स जेनरेट करने में लगने वाले समय से बहुत कम थी। जब तक मॉडल ने काम शुरू किया, लॉक पहले ही गायब हो चुका था, इसलिए किसी भी चीज़ ने दूसरे रिक्वेस्ट को उसी कन्वर्सेशन रिकॉर्ड को पकड़ने और एक और रिप्लाई देने से नहीं रोका।
दो लक्षण सामने आए:
- एक के बाद एक बिल्कुल एक जैसे रिप्लाई भेजे गए।
- एक ही सवाल के लिए थोड़े अलग शब्दों में रिप्लाई दिखाई दिए, क्योंकि प्रत्येक प्रोसेस ने यूजर के एक ही इनपुट से अपना प्रॉम्प्ट तैयार किया था।
चूंकि अधिकांश यूजर मैसेज के बीच में रुकते हैं, इसलिए यह बग नजरों से बचा रहा। केवल तेजी से टाइप करने वाले लोग ही रेस कंडीशन (race condition) को ट्रिगर कर पाते थे, और ऐसे मामले दुर्लभ थे।
वह अधूरा समाधान जो काम नहीं आया
पहला समाधान मैसेज आने के बाद एक छोटा सा डिले (delay) जोड़ना था, इस उम्मीद में कि तेजी से आने वाले इनपुट को "डिबाउंस" (debounce) किया जा सके। इससे तब मदद मिली जब दो मैसेज एक के बाद एक आए, लेकिन यदि AI अभी टेक्स्ट जेनरेट ही कर रहा हो और तभी तीसरा मैसेज आ जाए, तो यह समाधान विफल हो गया।
एक दूसरी समस्या तब सामने आई जब टाइमर और कन्वर्सेशन डेटा एक ही स्टोरेज बकेट में रखे गए। जब बॉट ने एक रिक्वेस्ट को प्रोसेस करना पूरा किया, तो उसने टाइमर रिकॉर्ड को ओवरराइट कर दिया, जिससे प्रभावी रूप से उसका अपना काउंटडाउन ही डिलीट हो गया। सिस्टम इस बात का ट्रैक खो बैठा कि किन मैसेज का जवाब पहले ही दिया जा चुका है, जिससे और अधिक डुप्लिकेशन की संभावना बढ़ गई।
एक भरोसेमंद सुरक्षा प्रणाली बनाना: वर्जन काउंटर, आइसोलेटेड टाइमर और एक लीज
टीम ने तीन स्तंभों के आधार पर फ्लो को फिर से डिजाइन किया:
- वर्जन काउंटर – प्रत्येक आने वाला मैसेज कन्वर्सेशन के साथ स्टोर किए गए एक काउंटर को बढ़ाता है। यह काउंटर सिस्टम को बताता है कि पिछले रिप्लाई के बाद से कितने मैसेज आ चुके हैं, जिससे रिस्पॉन्स जेनरेट होते समय नए इनपुट का पता लगाना आसान हो जाता है।
- डेडिकेटेड डिबाउंस विंडो – टाइमर अब एक अलग स्टोरेज एरिया में रहते हैं, जो कन्वर्सेशन पेलोड से अलग (insulated) होते हैं। डिबाउंस अवधि पर एक हार्ड कैप (hard cap) यूजर को बॉट को अनिश्चित काल तक रोकने से रोकता है।
- सेशन लीज – मूल लॉक को एक लीज से बदल दिया गया है जिसमें एक स्पष्ट एक्सपायरी टाइमस्टैम्प (expiry timestamp) होता है। लीज को 'कंपेयर-एंड-स्वैप' (CAS) ऑपरेशन का उपयोग करके क्लेम किया जाता है: प्रोसेस वर्तमान लीज वैल्यू को पढ़ता है, और केवल तभी नई वैल्यू लिखता है यदि पुरानी वैल्यू मैच करती है, और इस तरह कन्वर्सेशन पर विशेष अधिकार प्राप्त कर लेता है। यदि प्रोसेस क्रैश हो जाता है, तो लीज अपने आप समाप्त हो जाती है, जिससे कन्वर्सेशन अगले हैंडलर के लिए उपलब्ध हो जाती है।
नया पाइपलाइन कैसे काम करता है
- मैसेज का आना – सिस्टम वर्जन काउंटर को बढ़ाता है और डिबाउंस टाइमर को (री)सेट करता है। यह AI को शुरू किए बिना तुरंत क्लाइंट को वापस रिस्पॉन्स दे देता है।
- टाइमर की समाप्ति – टाइमर हैंडलर लीज प्राप्त करने का प्रयास करता है। यदि CAS सफल होता है, तो हैंडलर आगे बढ़ता है; अन्यथा वह पीछे हट जाता है, यह जानते हुए कि कोई अन्य प्रोसेस पहले से ही कन्वर्सेशन को संभाल रहा है।
- नए इनपुट की जांच – हैंडलर वर्तमान वर्जन काउंटर की तुलना उस वैल्यू से करता है जिसे उसने टाइमर शुरू होने पर रिकॉर्ड किया था। यदि काउंटर बढ़ गया है, तो वह पेंडिंग मैसेज को एक सिंगल प्रॉम्प्ट में इकट्ठा कर लेता है।
- रिप्लाई जेनरेट करना – AI मॉडल एक बार चलता है, जिससे एक सिंगल जवाब मिलता है जो सभी हालिया यूजर इनपुट्स को कवर करता है।
- अंतिम सैनिटी चेक – रिप्लाई भेजने से ठीक पहले, हैंडलर वर्जन काउंटर को फिर से पढ़ता है। यदि जनरेशन के दौरान कोई नया मैसेज आया है, तो रिप्लाई को हटा दिया जाता है और प्रोसेस टाइमर को फिर से शुरू कर देता है, जिससे यह सुनिश्चित होता है कि कोई पुराना जवाब यूजर तक न पहुंचे।
यह दृष्टिकोण डुप्लिकेट रिप्लाई को खत्म करता है, कन्वर्सेशन के रुकने के समय को सीमित करता है, और प्रोसेस क्रैश होने पर अपने आप रिकवर हो जाता है क्योंकि लीज अपने आप समाप्त हो जाती है।
निष्कर्ष
एक लॉक जो क्रिटिकल सेक्शन शुरू होने से पहले ही गायब हो जाता है, वह कोई सुरक्षा प्रदान नहीं करता है। एक अल्पकालिक डेटाबेस लॉक को एक स्पष्ट, समाप्त होने वाली लीज से बदलकर और टाइमर को कन्वर्सेशन डेटा से अलग करके, बॉट अब यह गारंटी देता है कि यूजर्स बहुत तेज गति से टाइप करने पर भी उन्हें एक सिंगल और अपडेटेड रिप्लाई मिले। यह घटना एक शाश्वत सबक को रेखांकित करती है: कन्करेंसी सुरक्षा उपायों (concurrency safeguards) को उस कार्य की अवधि से अधिक समय तक टिके रहना चाहिए जिसकी वे रक्षा कर रहे हैं, अन्यथा वे अदृश्य बाधाएं बन जाते हैं जो बग्स को फिसलने का मौका दे देते हैं।
