Oluwa.io
सभी लेखन
·9 मिनट पढ़ेंइंजीनियरिंगक्रिप्टोविश्वसनीयताट्रेडिंग

भरोसेमंद क्रिप्टो ऑर्डर एग्ज़ीक्यूशन शिप करना

क्रिप्टो ऑर्डर एग्ज़ीक्यूशन की विश्वसनीयता स्टेट मशीन, आइडेम्पोटेंसी कुंजियों और लगातार रिकंसीलिएशन से आती है। MadaiOps बनाने से मिले सबक।

द्वारा Vitor Lima

ज़्यादातर क्रिप्टो ट्रेडिंग उत्पाद स्पीड और एक चार्ट बेचते हैं। मुश्किल हिस्सा कैंडल बनाना नहीं है। मुश्किल है किसी भी मिलीसेकंड पर एक धोखेबाज़ी से सरल दिखने वाले सवाल का जवाब दे पाना: मेरे ऑर्डर की सच्ची स्थिति अभी इस समय क्या है? जब हमने अपना आंतरिक ऑर्डर और ट्रेडिंग-ऑपरेशंस ऐप MadaiOps बनाना शुरू किया, तो इसी सवाल ने सब कुछ व्यवस्थित किया। स्पीड एक फ़ीचर है। सच्चाई उत्पाद है। क्रिप्टो ऑर्डर एग्ज़ीक्यूशन की विश्वसनीयता वह चीज़ है जो तब आपके पास होती है जब ऐप उस सवाल का ईमानदारी से जवाब दे सके, भले ही वेन्यू खराब हो रहा हो; और जो ऐप ऐसा फ़िल दिखाए जो हुआ ही नहीं — या जो हुआ उसे छिपा दे — वह भरोसे को इस तरह तोड़ता है जिसकी भरपाई कोई भी लेटेंसी की जीत नहीं कर सकती।

यह पोस्ट उस इंजीनियरिंग के बारे में है जो एक ऑर्डर ऐप को भरोसेमंद बनाती है: स्टेट मशीन, आइडेम्पोटेंसी, WebSockets पर रिकंसीलिएशन, और वे फ़ेलियर मोड जो केवल लोड के तहत ही सामने आते हैं। हम इन सब को लेकर अपनी स्पष्ट राय रखते हैं, क्योंकि उबाऊ लगने वाले चुनाव ही आपको सॉल्वेंट बनाए रखते हैं।

ऑर्डर को एक स्पष्ट स्टेट मशीन के रूप में मॉडल करें

हमने जो सबसे अच्छा फ़ैसला लिया वह था ऑर्डर को एक ऐसी पंक्ति मानना बंद करना जिसे आप UPDATE करते हैं, और उसे नामित, गणित (enumerated) ट्रांज़िशन वाली एक फ़ाइनाइट स्टेट मशीन के रूप में मानना शुरू करना। एक ऑर्डर PENDING_NEW, WORKING, PARTIALLY_FILLED, FILLED, CANCEL_REQUESTED, CANCELED, REJECTED, और EXPIRED जैसी स्थितियों में रहता है। हर ट्रांज़िशन वर्तमान स्थिति और एक आने वाली घटना का फलन (function) होता है, और अवैध ट्रांज़िशन को चुपचाप निगलने के बजाय ज़ोर से अस्वीकार किया जाता है।

फ़ायदा: अस्पष्टता रात के 2 बजे की घटना बनने के बजाय वैलिडेशन-टाइम की चिंता बन जाती है। कोई FILLED ऑर्डर कभी वापस WORKING में नहीं जा सकता। किसी CANCELED ऑर्डर के लिए आने वाला फ़िल इवेंट "असंभव" नहीं है — यह एक असली रेस है, और स्टेट मशीन आपको यह तय करने पर मजबूर करती है कि इसका क्या मतलब है। आमतौर पर इसका मतलब होता है कि कैंसल रेस हार गया, ऑर्डर असल में फ़िल हो गया, और आपका लोकल CANCELED गलत था। ऑर्डर को म्यूटेबल पंक्तियों के रूप में मॉडल करें तो वह इवेंट बस किसी चीज़ को ओवरराइट कर देता है; और आपको इसका पता तब तक नहीं चलता जब तक रिकंसीलिएशन — या कोई उपयोगकर्ता — इसे पकड़ न ले।

निचोड़

एक भी API कॉल लिखने से पहले अपनी स्थितियों को गिनें (enumerate करें)। अगर आप व्हाइटबोर्ड पर डायग्राम नहीं बना सकते, तो आप अभी अपने डोमेन को नहीं समझते, और एक्सचेंज आपको मुश्किल तरीके से सिखाएगा। केवल वर्तमान स्थिति नहीं, बल्कि ट्रांज़िशन स्टोर करें। जो हुआ उसका एक अपेंड-ओनली इवेंट लॉग एक अकेले म्यूटेबल स्टेटस कॉलम से बेहतर है, क्योंकि लॉग को दोबारा चलाया जा सकता है और कॉलम एक अनुमान है।

आइडेम्पोटेंसी वैकल्पिक नहीं है

नेटवर्क रीट्राई करते हैं। आपका अपना रीट्राई लॉजिक रीट्राई करता है। कोई उपयोगकर्ता "बाय" पर दो बार टैप कर देता है। इनमें से कोई भी एक ही ऑर्डर को दो बार भेज सकता है, और एक्सचेंज पर एक डुप्लिकेट ऑर्डर असली पैसा है। इसका बचाव है एक क्लाइंट-जनित आइडेम्पोटेंसी कुंजी — ज़्यादातर एक्सचेंज इसे clientOrderId या newClientOrderId कहते हैं — जो हर ऑर्डर के साथ उसी क्षण जुड़ जाती है जब उपयोगकर्ता कमिट करता है, इससे पहले कि अनुरोध डिवाइस या सर्वर से बाहर निकले।

हम जो नियम लागू करते हैं: कुंजी एक बार जनित होती है, नेटवर्क कॉल से पहले स्थानीय रूप से persist की जाती है, और हर रीट्राई पर हूबहू उसी का पुनः उपयोग किया जाता है। अगर पहला प्रयास बिना किसी जवाब के टाइमआउट हो गया, तो रीट्राई वही कुंजी लेकर जाता है, और एक्सचेंज या तो ऑर्डर एक बार बनाता है या मौजूदा ऑर्डर लौटा देता है। किसी भी हाल में आप एक ही ऑर्डर पर पहुँचते हैं। कुंजी को फ़ेलियर के बाद जनित करें और आपने एक डुप्लिकेट-ऑर्डर मशीन बना ली है।

यह कैंसल और मॉडिफ़िकेशन तक फैलता है। एक कैंसल अनुरोध भी आइडेम्पोटेंट होना चाहिए: पहले से कैंसल हो चुके ऑर्डर को कैंसल करना एक नो-ऑप है जो सफलता लौटाता है, न कि कोई त्रुटि जो एक और रीट्राई लूप शुरू कर दे। हम "इच्छित अंतिम स्थिति पहले से ही सच है" को हर जगह सफलता मानते हैं। यह मामूली लगता है। यह रीट्राई तूफ़ानों की एक पूरी श्रेणी को खत्म कर देता है।

निचोड़

आइडेम्पोटेंसी कुंजी को कार्य करने से पहले persist करें, सफल होने के बाद नहीं। "मैंने इसे भेजा" और "मुझे पता है कि यह काम कर गया" के बीच का अंतराल ठीक वही जगह है जहाँ पैसा गायब होता है, और कुंजी ही एकमात्र चीज़ है जो उस अंतराल को बंद करती है।

लगातार रिकंसाइल करें: WebSocket एक संकेत है, सच्चाई नहीं

एक्सचेंज WebSockets पर ऑर्डर अपडेट पुश करते हैं, और जब वे काम करते हैं तो शानदार होते हैं: सब-सेकंड फ़िल, लाइव पार्शियल-फ़िल मात्राएँ, कैंसल की पुष्टियाँ। लेकिन WebSocket एक ऐसी स्ट्रीम है जिसे आप मिस कर सकते हैं। आप डिस्कनेक्ट होंगे। संदेश क्रम से बाहर आते हैं, रीकनेक्ट के दौरान गिर जाते हैं, या केवल इसलिए कभी नहीं आते क्योंकि वेन्यू खराब स्थिति में है। अगर आपकी ऑर्डर स्थिति केवल वही है जो सॉकेट ने आखिरी बार आपको बताई, तो सॉकेट के हिचकोले खाते ही आपका ऐप पूरे आत्मविश्वास के साथ गलत हो जाता है।

इसलिए हम दो लूप चलाते हैं। तेज़ लूप WebSocket का उपभोग करता है और इवेंट्स को आशावादी (optimistically) रूप से लागू करता है। धीमा लूप REST API को पोल करता है — GET /order और ओपन-ऑर्डर एंडपॉइंट्स — एक अंतराल पर और हर रीकनेक्ट के बाद, फिर उस स्नैपशॉट को लोकल स्थिति के विरुद्ध रिकंसाइल करता है। REST सच्चाई का स्रोत है; सॉकेट एक कम-लेटेंसी वाला संकेत है। जब वे असहमत होते हैं, तो REST जीतता है और हम विचलन (divergence) को लॉग करते हैं, क्योंकि एक विचलन एक बग रिपोर्ट है जो सिस्टम ने आपके लिए लिख दी।

क्रम मायने रखता है। ज़्यादातर एक्सचेंज अपडेट्स पर एक मोनोटॉनिक सीक्वेंस नंबर या एक updateTime की मुहर लगाते हैं। उस ऑर्डर के लिए हम आखिरी लागू किए गए इवेंट से पुराने किसी भी इवेंट को गिरा देते हैं, जिससे क्रम-रहित डिलीवरी हानिरहित हो जाती है। रीकनेक्ट पर सही कदम है: दोबारा सब्सक्राइब करें, अंतराल को भरने के लिए तुरंत एक पूरा REST स्नैपशॉट खींचें, फिर लाइव स्ट्रीम पर दोबारा भरोसा करें। स्नैपशॉट छोड़ दें और आप उस फ़िल को मिस कर देते हैं जो उन दो सेकंडों के दौरान हुआ जब आप ऑफ़लाइन थे।

निचोड़

किसी रियल-टाइम फ़ीड को कभी भी स्थिति का अकेला लेखक न बनने दें। सॉकेट आपको तेज़ बनाता है; रिकंसाइल लूप आपको सही बनाता है। आपको दोनों की ज़रूरत है, और जब वे टकराते हैं, तो हर बार सही, तेज़ को हरा देता है।

पार्शियल फ़िल और डिस्कनेक्ट: वे दो जो आपको उजागर कर देते हैं

पार्शियल फ़िल वह जगह है जहाँ भोले ऐप चुपचाप झूठ बोलते हैं। 1.0 BTC का एक ऑर्डर जो 0.3, फिर 0.4 फ़िल होता है, और फिर अटक जाता है, वह न "ओपन" है और न "पूरा"। वह PARTIALLY_FILLED है जिसमें 0.7 एग्ज़ीक्यूट हुआ और 0.3 बाकी है, और इनमें से हर संख्या को अलग-अलग फ़िल इवेंट्स से ट्रैक करना होता है, न कि किसी एक स्टेटस फ़ील्ड से अनुमानित करना होता है। हम अलग-अलग फ़िल्स से एग्ज़ीक्यूटेड मात्रा और वॉल्यूम-वेटेड औसत मूल्य को संचित (accumulate) करते हैं, ताकि स्थिति को हमेशा मूल तत्वों (primitives) से पुनर्निर्मित किया जा सके। जो उपयोगकर्ता "फ़िल्ड" देखता है, उसे यह जानना होता है कि फ़िल्ड कितना, किस औसत मूल्य पर। इससे कम कुछ भी एक अधूरा सच है।

डिस्कनेक्ट इसे और जटिल कर देते हैं। फ़िल के बीच में ऑफ़लाइन गिर जाएँ और WebSocket ने आपको 0.4 के बारे में कभी बताया ही नहीं, इसलिए आपकी लोकल स्थिति बासी है। ठीक इसीलिए रीकनेक्ट-फिर-स्नैपशॉट का अनुशासन मौजूद है: REST स्नैपशॉट संचयी एग्ज़ीक्यूटेड मात्रा लौटाता है, और रिकंसाइल लूप उस अंतराल को बंद कर देता है बिना उपयोगकर्ता को कोई गलत संख्या दिखाए। जिस फ़ेलियर मोड के विरुद्ध हम डिज़ाइन करते हैं वह "ऐप क्रैश हो गया" नहीं है — क्रैश ईमानदार होते हैं। वह है "ऐप चलता रहा और कुछ झूठा दिखाता रहा।"

निचोड़

संचयी एग्ज़ीक्यूटेड मात्रा को प्राथमिक तथ्य के रूप में ट्रैक करें और बाकी सब कुछ उसी से निकालें। "बाकी" (remaining) को कभी भी एक स्वतंत्र फ़ील्ड के रूप में स्टोर न करें जिसे आप म्यूटेट करते हैं। इसे निकालें (ऑर्डर किया गया − एग्ज़ीक्यूटेड) ताकि यह हकीकत से बेतालमेल न हो सके।

दबाव में सच्चा होना चतुर होने से बेहतर है

चतुर होने का एक लगातार प्रलोभन रहता है: पुष्टि से पहले फ़िल की भविष्यवाणी करना, "क्षणिक" त्रुटियों को छिपाना, रीकनेक्ट को इतना चिकना कर देना कि UI कभी टिमटिमाए ही नहीं। हमने इसमें से ज़्यादातर का विरोध करना सीख लिया है। एक ट्रेडिंग ऑपरेटर एक चिकना झूठ नहीं चाहता; वह ईमानदारी से जानना चाहता है कि कनेक्शन अभी गिरा और स्थिति रिकंसाइल हो रही है। एक दिखाई देने वाली "रीकनेक्ट हो रहा है, ऑर्डर सत्यापित कर रहे हैं" स्थिति उस निर्बाध UI से ज़्यादा मूल्यवान है जो असल में कल का सच दिखा रहा हो।

ठोस रूप में, हम अनिश्चितता को छिपाने के बजाय उसे सामने लाते हैं। जब लोकल स्थिति और एक्सचेंज हाल ही में रिकंसाइल नहीं हुए, तो ऐप ऐसा कहता है। जब कोई ऑर्डर CANCEL_REQUESTED है पर अभी तक कैंसल की पुष्टि नहीं हुई, तो यह ठीक वही दिखाता है, समय से पहले CANCELED नहीं। ऑप्टिमिस्टिक UI किसी टू-डू ऐप के लिए ठीक है। पैसे के लिए, आशावाद एक देनदारी है जिसकी कीमत आप सपोर्ट टिकटों और खोए हुए भरोसे में चुकाते हैं।

निचोड़

संदेह होने पर, संदेह दिखाएँ। दबाव में कोई भी वित्तीय ऐप सबसे आश्वस्त करने वाली चीज़ यही कर सकता है कि अनुमान लगाने से इनकार कर दे।

समापन विचार

इसमें से कुछ भी चमकदार नहीं है। ऐसा कोई डेमो नहीं होता जहाँ "हमारी आइडेम्पोटेंसी कुंजियाँ नेटवर्क कॉल से पहले persist होती हैं" पर तालियाँ बजें। लेकिन विश्वसनीयता कोई फ़ीचर नहीं है जिसे आप बाद में जोड़ते हैं। यह उन फ़ैसलों का समूह है जो आप जल्दी करते हैं और समझौता करने से इनकार कर देते हैं, फिर हर बार जब कोई शॉर्टकट लुभावना लगे तब उसकी रक्षा करते हैं। ऐसा सिस्टम बनाएँ जो तब सच बोले जब सब कुछ आग में हो, और तेज़, सुंदर हिस्से खुद-ब-खुद संभल जाएँगे।