एक यूजर बटन पर क्लिक करता है। रिक्वेस्ट अटक जाती है। दस सेकंड की खामोशी। वे फॉलबैक बटन पर क्लिक करते हैं। अब एक ही इरादे (intention) के लिए दो जॉब्स चल रहे हैं। इसका परिणाम डुप्लिकेट साइड इफेक्ट्स, डबल चार्ज और डेटा का ऐसा कचरा होता है जो आपका पूरा दोपहर खराब कर देता है।
यह कोई फ्रंटएंड बग नहीं है। React में एक डिसेबल्ड बटन या डिबाउंस टाइमर (debounce timer) आपको नहीं बचा पाएगा। पहली रिक्वेस्ट पहले से ही प्रोसेस में थी। नेटवर्क ने बस रिस्पॉन्स को निगल लिया। यदि आपका बैकएंड हर आने वाली रिक्वेस्ट को एक बिल्कुल नई इंस्ट्रक्शन मानता है, तो रिट्राइज़ (retries) आपके लिए मुसीबत बन जाते हैं। आपको इसे अपने API डिज़ाइन और डेटाबेस स्कीमा में ठीक करने की ज़रूरत है।
समाधान एक सरल स्ट्रक्चरल विभाजन (structural split) से शुरू होता है।
Jobs को Attempts से अलग करें
एक 'job' को उस चीज़ के टिकाऊ रिकॉर्ड के रूप में सोचें जो यूजर चाहता है। यह ओनर (owner), पैरामीटर्स, टारगेट प्रोवाइडर और सटीक इरादे (intent) को कैप्चर करता है। एक 'attempt' उस इरादे को पूरा करने का एक विशिष्ट प्रयास है।
एक प्रिंट शॉप की कल्पना करें। आप एक फाइल सौंपते हैं और वे आपको टिकट #45 देते हैं। वह टिकट 'job' है। शॉप इंकजेट प्रिंटर आज़माती है। वह जाम हो जाता है। यह पहला 'attempt' है। वे फाइल को लेज़र प्रिंटर पर ले जाते हैं। यह दूसरा 'attempt' है। पूरी प्रक्रिया के दौरान, टिकट #45 कभी नहीं बदलता। यदि शॉप हर बार नए प्रिंटर के लिए नया टिकट जारी करती, तो आपको तीन बार भुगतान करना पड़ता और तीन अनचाही प्रतियां मिलतीं।
आपके डेटाबेस को भी इसी तरह होना चाहिए। एक टेबल 'jobs' को रखती है। दूसरी टेबल 'attempts' को रखती है। 'job' रो (row) स्थिर रहती है जबकि उसके नीचे 'attempts' जमा होते जाते हैं।
यह अलगाव आपको नियंत्रण देता है। यह आपको एक 'idempotency key' जोड़ने की जगह भी देता है जो नेटवर्क की समस्याओं (network blips) के बावजूद बनी रहती है।
हर Job पर Idempotency Key अनिवार्य करें
हर POST रिक्वेस्ट जो एक जॉब बनाती है, उसमें एक यूनिक 'idempotency key' होनी चाहिए। यह की (key) यूजर की होती है, सेशन की नहीं। ओनर ID और की को मिलाएं, फिर उन दोनों कॉलमों पर एक यूनिक डेटाबेस कंस्ट्रेंट (unique database constraint) लागू करें।
डेटाबेस कंस्ट्रेंट क्यों? क्योंकि इंसर्ट करने से पहले एप्लिकेशन कोड में अस्तित्व (existence) की जांच करना एक 'race condition' है जो कभी भी समस्या पैदा कर सकती है। दो एक जैसी रिक्वेस्ट एक ही माइक्रोसेकंड के अंतराल में निकल सकती हैं। डेटाबेस को ही लागू करने दें (enforcer)। यदि कोई यूजर एक ही ओनर ID और की को दो बार भेजता है, तो दूसरी रिक्वेस्ट यूनिक वायलेशन (unique violation) को पकड़ लेगी और आप मौजूदा जॉब वापस कर देंगे। दोनों रिक्वेस्ट को एक ही जॉब ID मिलेगी। कोई डुप्लिकेट काम शुरू नहीं होगा।
स्कोप (scope) के मामले में सख्त रहें। यदि कोई की का पुन: उपयोग करता है लेकिन इनपुट पेलोड बदल देता है, तो 'conflict' रिटर्न करें। Idempotency key को केवल यूजर से नहीं, बल्कि सटीक इरादे (exact intention) से बंधा होना चाहिए। अलग इनपुट के साथ एक ही की का मतलब है कि क्लाइंट भ्रमित है, और आपके सिस्टम को अनुमान लगाने के बजाय इसे रिजेक्ट कर देना चाहिए।
State Transitions को सुरक्षित रखें
एक 'attempt' एक स्टेट ट्रांजिशन (state transition) है, न कि एक नया जॉब। यदि पिछला प्रयास अभी भी 'starting' या 'unknown' स्टेट में अटका हुआ है, तो आपके API को नया प्रयास शुरू करने से मना कर देना चाहिए।
टाइमआउट इसका कारण हैं। जब किसी प्रोवाइडर रिक्वेस्ट का टाइमआउट हो जाता है, तो क्लाइंट को विफलता (failure) दिखती है, लेकिन सर्वर-साइड प्रोसेस अभी भी जीवित हो सकती है। GPU क्लस्टर अभी भी आपके इन्फरेंस (inference) रिक्वेस्ट पर काम कर रहा हो सकता है। कंटेनर अभी भी ब्लब स्टोरेज (blob storage) में लिख रहा हो सकता है। यदि आप टाइमआउट हुए प्रयास को 'failed' के रूप में मार्क करते हैं और तुरंत दूसरा प्रयास शुरू कर देते हैं, तो आप डुप्लिकेट साइड इफेक्ट्स के साथ जुआ खेल रहे हैं।
टाइमआउट को 'failed' के बजाय 'unknown' स्टेट के रूप में मानें। जब तक पिछला प्रयास किसी 'terminal state' तक नहीं पहुँच जाता या किसी बाहरी प्रक्रिया द्वारा स्पष्ट रूप से रद्द (cancel) नहीं कर दिया जाता, तब तक नए प्रयासों को रोक दें। यह ठहराव असहज हो सकता है। यह यूजर को इंतजार करने के लिए मजबूर करता है। यह दो वर्कर्स द्वारा एक ही डाउनस्ट्रीम रिसोर्सेज को बदलने (mutating) की अराजकता को भी रोकता है।
Compare-and-Swap के साथ Races को हल करें
सबसे कठिन समस्याएँ तब सामने आती हैं जब कई प्रयास (attempts) समाप्त होते हैं। हो सकता है कि आपके सिस्टम ने प्राइमरी प्रोवाइडर के खिलाफ पहला प्रयास किया हो। दस सेकंड की खामोशी के बाद, इसने फॉलबैक के खिलाफ दूसरा प्रयास किया। अब दोनों प्रयास पूरे हो चुके हैं। आप दोनों को एक ही जॉब रो में अपने परिणाम लिखने की अनुमति नहीं दे सकते।
'Compare-and-swap' लॉजिक का उपयोग करें। जॉब रो में एक वर्जन नंबर (version number) जोड़ें। जब एक प्रयास समाप्त होता है, तो यह कुछ शर्तों के साथ एक अपडेट चलाता है:
- वर्तमान वर्जन वही होना चाहिए जो प्रयास ने शुरुआत में पढ़ा था।
- किसी अन्य प्रयास ने पहले से ही परिणाम स्लॉट (result slot) पर दावा न किया हो।
- यदि दोनों शर्तें पूरी होती हैं, तो परिणाम लिखें और वर्जन बढ़ा दें।
SQL के संदर्भ में, यह एक WHERE id = $1 AND version = $2 AND completed_by IS NULL वाले अपडेट स्टेटमेंट जैसा दिखता है। यदि अपडेट शून्य रो (zero rows) लौटाता है, तो इसका मतलब है कि किसी अन्य प्रयास ने पहले ही जीत हासिल कर ली है। देर से आने वाली रिक्वेस्ट को अनदेखा किया जाना चाहिए। उसके परिणाम को छोड़ दें। मर्ज न करें। अपेंड (append) न करें। काम को खारिज कर दें। एक देर से आया परिणाम जो पहले से विजेता को ओवरराइट कर देता है, वह डेटा करप्शन (data corruption) है, और एकमात्र सुरक्षित कदम इसे हटा देना है।
यह उलटे क्रम में समाप्त होने (reverse-order finish) की स्थिति को सफाई से संभालता है। प्रयास A पहले निकलता है लेकिन तीस सेकंड बाद वापस आता है। प्रयास B दूसरे नंबर पर निकलता है लेकिन पांच सेकंड बाद वापस आता है। प्रयास B 'compare-and-swap' जीत जाता है। प्रयास A का अपडेट शून्य पंक्तियों (zero rows) को प्रभावित करता है। आपका सिस्टम इस रेस (race) को लॉग करता है, पुराने (stale) पेलोड को अनदेखा करता है, और आगे बढ़ जाता है।
ब्रेकपॉइंट्स का परीक्षण करें
आप इन बग्स को 'happy-path testing' में नहीं पकड़ पाएंगे। आपके टेस्ट सुइट को सिस्टम की कमजोरियों (fractures) को लक्षित करने की आवश्यकता है।
- एक डबल-क्लिक का अनुकरण (simulate) करें। एक ही idempotency key के साथ दो एक साथ आने वाले POST अनुरोधों को समान job IDs वापस करने चाहिए।
- बेमेल इनपुट के साथ वही की (key) भेजें। एक 'conflict response' की अपेक्षा करें। यदि पैरामीटर अलग हैं, तो सिस्टम को चुपचाप मौजूदा जॉब वापस नहीं करना चाहिए।
- टाइमआउट (timeout) पैदा करें। सत्यापित करें कि जॉब एक 'unknown state' में पहुँचता है, न कि 'failed state' में, और सिस्टम तब तक आगे के प्रयासों को रोकता है जब तक कि अस्पष्टता दूर न हो जाए।
- दो प्रयासों को उलटे क्रम में समाप्त होने के लिए मजबूर करें। पुष्टि करें कि वापस आने वाला दूसरा प्रयास हार जाता है, भले ही निकलने वाला पहला प्रयास आधिकारिक प्राथमिक प्रदाता (primary provider) रहा हो।
ये टेस्ट केवल 'edge-case' की विलासिता नहीं हैं। ये वह अनुबंध (contract) हैं जो आपका API सिस्टम के बाकी हिस्सों के साथ बनाता है।
फेलओवर (Fail Over) करने से पहले प्रदाता के इरादे (Provider Intent) को सत्यापित करें
यदि आप मल्टी-प्रदाता सेटअप चलाते हैं, तो आप विभिन्न AI मॉडलों को एक-दूसरे के स्थान पर बदलने योग्य स्लॉट के रूप में मानने के लिए प्रलोभित हो सकते हैं। वे एक ही कोड पाथ, एक ही HTTP क्लाइंट और एक ही JSON स्कीमा साझा करते हैं। इसका मतलब यह नहीं है कि वे एक जैसा व्यवहार करते हैं।
एक मॉडल टॉप-लेवल की (key) को 'hallucinate' कर सकता है। दूसरा आपके सिस्टम प्रॉम्प्ट फॉर्मेटिंग को अनदेखा कर सकता है। स्कीमा वैलिडेशन सिंटैक्स त्रुटियों को पकड़ लेता है, लेकिन यह ऐसे रिस्पॉन्स को पास कर देगा जिसे आपका बिजनेस लॉजिक समझ नहीं पाएगा। एक प्रदाता वैध JSON लौटा सकता है जो आपके प्रॉम्प्ट टेम्पलेट के साथ गलत काम करता है।
ऑटोमैटिक मॉडल स्विचिंग की अनुमति देने से पहले प्रदाता-विशिष्ट (provider-specific) टेस्ट चलाएं। पुष्टि करें कि 'fallback model' वास्तव में कम तापमान (low temperature) पर आपके आउटपुट स्ट्रक्चर का सम्मान करता है। सत्यापित करें कि आपका प्रॉम्प्ट उस प्रदाता के टोकनाइज़र (tokenizer) के माध्यम से सही ढंग से रेंडर होता है। वास्तविक इनपुट के साथ पूरे 'round trip' का परीक्षण करें। ऑटोमैटिक फेलओवर तभी सुरक्षित है जब आपने यह साबित कर दिया हो कि 'fallback' मॉडल भी उसी ऑपरेशनल कॉन्ट्रैक्ट का पालन करता है।
प्रति इरादा (Intent) एक ही जॉब रखें
फ़ॉलबैक पाथ (Fallback paths) अच्छे हैं। अनियंत्रित फ़ॉलबैक गुणन (multiplication) एक बग है। आपके स्टैक के हर स्तर को यह मूल्यांकन करने की आवश्यकता है कि क्या उसने पहले ही सटीक कार्य देख लिया है। लोड बैलेंसर, API हैंडलर, डेटाबेस और वर्कर, सभी को एक ही पहचान (identity) का सम्मान करना चाहिए।
अपने सिस्टम को इस तरह बनाएं कि रिट्राइ (retries) और फ़ॉलबैक एक ही स्थिर जॉब के तहत नए प्रयासों के रूप में सामने आएं। डेटाबेस-आधारित idempotency key के साथ जॉब को लॉक करें। ट्रांज़िशन (transitions) की रक्षा करें। प्रयासों के बीच प्रतिस्पर्धा (race) होने दें। ठीक एक को ही जीतने दें। इस तरह आप एक सिंगल यूजर क्लिक को डेटा क्लीनअप के पूरे वीकेंड में बदलने से रोक सकते हैं।
