वापरकर्ता एका बटणावर क्लिक करतो. विनंती (request) थांबते. दहा सेकंदांची शांतता. ते फॉलबॅक (fallback) बटणावर क्लिक करतात. आता एकाच हेतूसाठी दोन कामे (jobs) सुरू होतात. परिणामी डुप्लिकेट साईड इफेक्ट्स (side effects), दुहेरी शुल्क आणि डेटाचा असा गोंधळ होतो जो तुमचा पूर्ण दुपार खराब करतो.
ही फ्रंटएंडमधील चूक (bug) नाही. React मधील डिसेबल केलेले बटण किंवा डिबाउन्स टाइमर (debounce timer) तुम्हाला वाचवू शकणार नाही. पहिली विनंती आधीच प्रक्रियेत (in flight) होती. नेटवर्कने फक्त प्रतिसाद (response) गिळंकृत केला. जर तुमचा बॅकएंड प्रत्येक येणाऱ्या विनंतीला एक नवीन सूचना मानत असेल, तर 'retries' हे तुमच्यासाठी ओझे ठरतील. तुम्हाला तुमच्या API डिझाइन आणि डेटाबेस स्कीमामध्ये हे सुधारण्याची गरज आहे.
याचे समाधान एका साध्या संरचनात्मक विभागापासून (structural split) सुरू होते.
Jobs आणि Attempts वेगळे करा
'Job' ला वापरकर्त्याला काय हवे आहे याचा एक कायमस्वरूपी रेकॉर्ड समजा. त्यात मालक (owner), पॅरामीटर्स, टार्गेट प्रोव्हायडर आणि नेमका हेतू (intent) साठवला जातो. 'Attempt' म्हणजे तो हेतू पूर्ण करण्याचा एक विशिष्ट प्रयत्न आहे.
एका प्रिंटिंग शॉपचे उदाहरण घ्या. तुम्ही एक फाईल देता आणि ते तुम्हाला तिकीट #45 देतात. ते तिकीट म्हणजे 'job' आहे. शॉप इंकजेट प्रिंटर वापरण्याचा प्रयत्न करते. ते अडकते (jam). हा पहिला 'attempt' आहे. ते फाईल लेझर प्रिंटरवर हलवतात. हा दुसरा 'attempt' आहे. संपूर्ण प्रक्रियेदरम्यान, तिकीट #45 कधीही बदलत नाही. जर शॉपने प्रत्येक प्रिंटरसाठी नवीन तिकीट दिले असते, तर तुम्हाला तीन वेळा पैसे द्यावे लागले असते आणि तीन नको असलेल्या प्रती मिळाल्या असत्या.
तुमच्या डेटाबेसमध्येही हेच असायला हवे. एक टेबल 'jobs' साठी असेल आणि दुसरे 'attempts' साठी. 'Job' ची रो (row) स्थिर राहते, तर त्याखाली 'attempts' जमा होत जातात.
हे विभाजन तुम्हाला नियंत्रण देते. तसेच नेटवर्कमधील अडथळ्यांनंतरही टिकून राहणारी 'idempotency key' जोडण्यासाठी तुम्हाला एक जागा देते.
प्रत्येक Job साठी Idempotency Key अनिवार्य करा
प्रत्येक POST विनंती, जी एक 'job' तयार करते, ती एक युनिक (unique) 'idempotency key' घेऊन असणे आवश्यक आहे. ही की (key) वापरकर्त्याची असते, सेशनची (session) नाही. 'owner ID' आणि 'key' एकत्र करा आणि त्या दोन कॉलम्सवर युनिक डेटाबेस कन्स्ट्रेंट (unique database constraint) लागू करा.
डेटाबेस कन्स्ट्रेंट का? कारण डेटा इन्सर्ट करण्यापूर्वी ॲप्लिकेशन कोडमध्ये अस्तित्वाची तपासणी करणे हे 'race condition' निर्माण करू शकते. दोन सारख्याच विनंत्या एकाच मायक्रोसेकंदच्या अंतरातून निसटून जाऊ शकतात. डेटाबेसला अंमलबजावणी करू द्या. जर वापरकर्त्याने तोच 'owner ID' आणि 'key' दोनदा पाठवला, तर दुसरी विनंती 'unique violation' पकडेल आणि तुम्ही अस्तित्वात असलेला 'job' परत कराल. दोन्ही विनंत्यांना तोच 'job ID' मिळेल. कोणतेही अतिरिक्त काम सुरू होणार नाही.
व्याप्तीबाबत (scope) कडक राहा. जर कोणी तीच की वापरली पण इनपुट पेलोड (payload) बदलला, तर 'conflict' एरर परत करा. 'Idempotency key' केवळ वापरकर्त्याशी नाही, तर नेमक्या हेतूशी (intent) जोडलेली असावी. वेगळ्या इनपुटसह तीच की वापरणे म्हणजे क्लायंट गोंधळलेला आहे, आणि तुमच्या सिस्टमने अंदाज लावण्याऐवजी ती विनंती नाकारली पाहिजे.
State Transitions सुरक्षित करा
'Attempt' हा एक 'state transition' आहे, नवीन 'job' नाही. जर मागील 'attempt' अजूनही 'starting' किंवा 'unknown' स्थितीत अडकलेला असेल, तर तुमच्या API ने नवीन 'attempt' सुरू करण्यास नकार दिला पाहिजे.
याचे कारण 'timeouts' आहेत. जेव्हा प्रोव्हायडरची विनंती 'timeout' होते, तेव्हा क्लायंटला ती अपयशी वाटते, परंतु सर्व्हर-साइड प्रक्रिया अजूनही सुरू असू शकते. GPU क्लस्टर अजूनही तुमच्या इन्फरन्स (inference) विनंतीवर काम करत असू शकते. कंटेनर अजूनही 'blob storage' मध्ये डेटा लिहित असू शकतो. जर तुम्ही 'timeout' झालेल्या 'attempt' ला 'failed' म्हणून मार्क केले आणि लगेच दुसरा 'attempt' सुरू केला, तर तुम्ही डुप्लिकेट साईड इफेक्ट्सचा धोका पत्करत आहात.
'Timeout' ला 'failed' न मानता 'unknown state' समजा. जोपर्यंत आधीचा 'attempt' अंतिम स्थितीत (terminal state) पोहोचत नाही किंवा एखाद्या बाह्य प्रक्रियेद्वारे (out-of-band process) स्पष्टपणे रद्द केला जात नाही, तोपर्यंत नवीन 'attempts' रोखून धरा. ही प्रतीक्षा वापरकर्त्यासाठी अस्वस्थ करणारी असू शकते आणि त्यांना थांबण्यास भाग पाडते. परंतु, यामुळे दोन वर्कर्स एकाच रिसोर्सेसमध्ये बदल करण्याचा गोंधळ टाळला जातो.
Resolve Races with Compare-and-Swap
सर्वात कठीण समस्या तेव्हा उद्भवतात जेव्हा एकाच वेळी अनेक 'attempts' पूर्ण होतात. कदाचित तुमच्या सिस्टमने प्राथमिक प्रोव्हायडरविरुद्ध पहिला 'attempt' केला असेल. दहा सेकंदांच्या शांततेनंतर, फॉलबॅक प्रोव्हायडरविरुद्ध दुसरा 'attempt' केला असेल. आता दोन्ही 'attempts' पूर्ण झाले आहेत. तुम्ही दोन्ही विनंत्यांना एकाच 'job row' मध्ये त्यांचे निकाल लिहू देऊ शकत नाही.
'Compare-and-swap' लॉजिक वापरा. 'Job row' मध्ये एक 'version number' जोडा. जेव्हा एखादा 'attempt' पूर्ण होतो, तेव्हा खालील अटींसह 'update' चालवा:
- सध्याची आवृत्ती (version) ही 'attempt' ने सुरुवातीला वाचलेल्या आवृत्तीशी जुळली पाहिजे.
- इतर कोणत्याही 'attempt' ने निकाल साठवण्याची जागा (result slot) आधीच घेतली नसावी.
- जर या दोन्ही अटी पूर्ण झाल्या, तर निकाल लिहा आणि 'version' वाढवा.
SQL च्या भाषेत, हे WHERE id = $1 AND version = $2 AND completed_by IS NULL असलेल्या 'update' स्टेटमेंटसारखे दिसते. जर 'update' मुळे शून्य रो (rows) परत आल्या, तर याचा अर्थ असा की दुसऱ्या एखाद्या 'attempt' ने आधीच विजय मिळवला आहे. उशिरा आलेल्या विनंतीकडे दुर्लक्ष केले पाहिजे. तिचा निकाल काढून टाका. तो विलीन (merge) करू नका किंवा जोडू (append) नका. ते काम फेकून द्या. आधीच्या विजेत्याचा निकाल बदलणारा उशिरा आलेला निकाल म्हणजे डेटा करप्शन (data corruption) आहे, आणि सुरक्षित मार्ग म्हणजे तो discard करणे हाच आहे.
हे उलट क्रमाने पूर्ण होणे (reverse-order finish) व्यवस्थित हाताळते. प्रयत्न A आधी निघतो पण तीस सेकंदांनंतर परत येतो. प्रयत्न B दुसऱ्या क्रमांकावर निघतो पण पाच सेकंदांनंतर परत येतो. प्रयत्न B 'compare-and-swap' मध्ये यशस्वी होतो. प्रयत्न A चे अपडेट शून्य ओळींवर (rows) परिणाम करते. तुमची प्रणाली ही स्पर्धा (race) नोंदवते, कालबाह्य (stale) payload दुर्लक्षित करते आणि पुढे जाते.
ब्रेकपॉइंट्सची चाचणी घ्या (Test the Breakpoints)
'happy-path' टेस्टिंगमध्ये तुम्हाला हे बग्स सापडणार नाहीत. तुमच्या टेस्टिंग सुईटला (test suite) कमकुवत दुव्यांवर (fractures) लक्ष केंद्रित करण्याची गरज आहे.
- डबल-क्लिकचे अनुकरण (Simulate) करा. एकाच idempotency key सह दोन एकाच वेळी येणाऱ्या POST विनंत्यांनी (requests) समान job IDs परत करणे आवश्यक आहे.
- विसंगत इनपुटसह तीच की (key) पाठवा. 'conflict response' ची अपेक्षा करा. जर पॅरामीटर्स वेगळे असतील, तर प्रणालीने शांतपणे अस्तित्वात असलेला जॉब परत करू नये.
- टाइमआउट (timeout) निर्माण करा. जॉब 'failed state' मध्ये न जाता 'unknown state' मध्ये जातो की नाही आणि जोपर्यंत संदिग्धता दूर होत नाही तोपर्यंत प्रणाली पुढील प्रयत्न रोखते की नाही, याची पडताळणी करा.
- दोन प्रयत्न उलट क्रमाने पूर्ण होण्यास भाग पाडा. जो दुसरा परत येईल तो पराभूत होतो, याची खात्री करा, जरी पहिला निघणारा अधिकृत प्राथमिक प्रदाता (primary provider) असला तरीही.
या चाचण्या केवळ 'edge-case' साठीच्या चैनीच्या गोष्टी नाहीत. ते तुमच्या API ने उर्वरित प्रणालीसोबत केलेले करार (contract) आहेत.
फेल-ओव्हर (Fail Over) करण्यापूर्वी प्रदात्याचा हेतू (Provider Intent) तपासा
जर तुम्ही मल्टी-प्रदाता (multi-provider) सेटअप चालवत असाल, तर तुम्हाला विविध AI मॉडेल्सना एकमेकांसाठी बदलता येण्याजोग्या स्लॉट्सप्रमाणे (interchangeable slots) मानण्याची इच्छा होऊ शकते. ते एकच कोड पाथ, एकच HTTP क्लायंट आणि एकच JSON स्कीमा वापरतात. याचा अर्थ असा नाही की त्यांचे वर्तन सारखेच असेल.
एखादे मॉडेल 'top-level key' मध्ये भ्रम (hallucinate) निर्माण करू शकते. दुसरे मॉडेल तुमच्या सिस्टम प्रॉम्प्ट फॉरमॅटिंगकडे दुर्लक्ष करू शकते. स्कीमा व्हॅलिडेशन (Schema validation) सिंटॅक्स त्रुटी पकडते, परंतु ते असे रिस्पॉन्स पास करू शकते ज्याचा तुमचा बिझनेस लॉजिक अर्थ लावू शकत नाही. एखादा प्रदाता वैध JSON परत करू शकतो, परंतु तो तुमच्या प्रॉम्प्ट टेम्पलेटसोबत चुकीची गोष्ट करू शकतो.
ऑटोमॅटिक मॉडेल स्विचिंगला परवानगी देण्यापूर्वी प्रदाता-विशिष्ट (provider-specific) चाचण्या करा. फॉलबॅक मॉडेल (fallback model) कमी तापमानावर (low temperature) तुमच्या आउटपुट स्ट्रक्चरचा खरोखर आदर करते की नाही याची खात्री करा. त्या प्रदात्याच्या टोकेनायझरद्वारे (tokenizer) तुमचा प्रॉम्प्ट योग्यरित्या रेंडर होतो की नाही याची पडताळणी करा. वास्तविक इनपुटसह संपूर्ण 'round trip' चाचणी करा. ऑटोमॅटिक फेलओव्हर तेव्हाच सुरक्षित असते जेव्हा तुम्ही हे सिद्ध करता की फॉलबॅक मॉडेल देखील त्याच ऑपरेशनल कराराचे (operational contract) पालन करते.
प्रत्येक हेतूसाठी (Intent) एकच जॉब ठेवा
फॉलबॅक पाथ्स (Fallback paths) चांगले आहेत. अनियंत्रित फॉलबॅक वाढणे हा एक बग आहे. तुमच्या स्टॅकच्या प्रत्येक थराला (layer) हे तपासण्याची गरज आहे की त्याने नेमके तेच कार्य आधी पाहिले आहे का. लोड बॅलन्सर, API हँडलर, डेटाबेस आणि वर्कर या सर्वांनी एकाच ओळखीचा (identity) आदर केला पाहिजे.
तुमची प्रणाली अशी बनवा की रिट्रायज (retries) आणि फॉलबॅक हे एका स्थिर जॉब अंतर्गत नवीन प्रयत्नांच्या स्वरूपात समोर येतील. डेटाबेस-आधारित idempotency key सह जॉब लॉक करा. ट्रान्झिशन्सचे (transitions) रक्षण करा. प्रयत्नांमध्ये स्पर्धा (race) होऊ द्या. फक्त एकच विजयी होऊ द्या. अशा प्रकारे तुम्ही वापरकर्त्याच्या एका क्लिकमुळे डेटा क्लीनअपसाठी अख्खा वीकेंड खर्च होण्यापासून वाचवू शकता.
