Rails विशिष्ट स्थितियों के लिए बहुत सारे उपयोगी बिल्ट-इन टूल्स के साथ एक बड़ा फ्रेमवर्क है। इस श्रृंखला में, हम Rails के बड़े कोडबेस में छिपे कुछ कम-ज्ञात टूल्स पर एक नज़र डालेंगे।
इस लेख में, हम Rails.cache में increment और decrement मेथड्स की व्याख्या करेंगे।
Rails.cache हेल्पर
Rails.cache आपके एप्लिकेशन में कैश के साथ इंटरैक्ट करने का प्रवेश द्वार है। यह एक अब्स्ट्रैक्शन भी है, जो आपको वास्तविक कैश "स्टोर" की परवाह किए बिना कॉल करने के लिए एक सामान्य API देता है। बॉक्स से बाहर, Rails निम्न का समर्थन करता है:
- FileStore
- MemoryStore
- MemCacheStore
- NullStore
- RedisCacheStore
Rails.cache का निरीक्षण करने से पता चलेगा कि आप कौन सा चला रहे हैं:
> Rails.cache
=> #nil, ...
हम उन सभी को विस्तार से नहीं देखेंगे, लेकिन त्वरित सारांश के रूप में:
- NullStore कुछ भी स्टोर नहीं करता; इससे वापस पढ़ने पर हमेशा
nilलौटेगा। यह नए Rails ऐप के लिए डिफ़ॉल्ट है। - FileStore कैश को हार्ड ड्राइव पर फाइलों के रूप में स्टोर करता है, इसलिए वे बने रहते हैं भले ही आप अपना Rails सर्वर रीस्टार्ट करें।
- MemoryStore कैश को RAM में रखता है, इसलिए यदि आप अपना Rails सर्वर रोकते हैं, तो आप कैश को भी मिटा देंगे।
- MemCacheStore और RedisCacheStore कैश को बनाए रखने के लिए बाहरी प्रोग्राम्स (क्रमशः MemCache और Redis) का उपयोग करते हैं।
विभिन्न कारणों से, यहाँ पहले तीन का उपयोग विकास/टेस्टिंग के लिए सबसे अधिक किया जाता है। प्रोडक्शन में, आप संभवतः Redis या MemCache का उपयोग करेंगे।
क्योंकि Rails.cache इन सेवाओं के बीच के अंतर को छुपाता है, अलग-अलग वातावरण में अलग-अलग वर्जन चलाना कोड बदले बिना आसान है; उदाहरण के लिए, आप विकास में NullStore, टेस्टिंग में MemoryStore, और प्रोडक्शन में RedisCacheStore का उपयोग कर सकते हैं।
कैश्ड डेटा
Rails के Rails.cache.read, Rails.cache.write, और Rails.cache.fetch के माध्यम से, हमारे पास कैश से किसी भी मनमाने डेटा को स्टोर और पुनर्प्राप्त करने का एक आसान तरीका है। एक पहले के लेख में इनको अधिक विस्तार से कवर किया गया था; इस लेख के लिए ध्यान देने वाली महत्वपूर्ण बात यह है कि इन मेथड्स पर कोई अंतर्निहित थ्रेड-सेफ्टी नहीं है। मान लीजिए हम एक रनिंग काउंट रखने के लिए कई थ्रेड्स से कैश्ड डेटा अपडेट कर रहे हैं; हमें रेस कंडीशन से बचने के लिए किसी प्रकार के लॉक में रीड/राइट ऑपरेशन को लपेटने की आवश्यकता होगी। इस उदाहरण पर विचार करें, यह मानते हुए कि हमने Redis कैश स्टोर का उपयोग करने के लिए चीजें सेट की हैं:
threads = []
# प्रारंभिक काउंटर सेट करें
Rails.cache.write(:test_counter, 0)
4.times do
threads << Thread.new do
100.times do
current_count = Rails.cache.read(:test_counter)
current_count += 1
Rails.cache.write(:test_counter, current_count)
end
end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter)
यहाँ हमारे पास चार थ्रेड्स हैं, प्रत्येक हमारे कैश्ड मान को सौ बार बढ़ा रहा है। परिणाम 400 होना चाहिए, लेकिन अधिकांश समय, यह बहुत कम होगा - मेरे टेस्ट रन पर 269। यहाँ क्या हो रहा है एक रेस कंडीशन है। मैंने इसे पिछले लेख में अधिक विस्तार से कवर किया था, लेकिन त्वरित सारांश के रूप में, क्योंकि थ्रेड्स सभी एक ही "साझा" डेटा पर काम कर रहे हैं, वे एक-दूसरे के साथ आउट ऑफ सिंक हो सकते हैं। उदाहरण के लिए, एक थ्रेड मान को पढ़ सकता है, फिर दूसरा थ्रेड ले लेता है और मान को भी पढ़ता है, इसे बढ़ाता है, और इसे स्टोर करता है। पहला थ्रेड फिर अपने अब-आउट-ऑफ-डेट मान का उपयोग करके फिर से शुरू होता है।
इस समस्या को हल करने का सामान्य तरीका कोड को म्यूचुअली एक्सक्लूसिव लॉक (या Mutex) से घेरना है ताकि एक समय में केवल एक थ्रेड लॉक के भीतर कोड निष्पादित कर सके। हमारे मामले में, हालांकि, Rails.cache के पास इस परिदृश्य को संभालने के लिए कुछ मेथड्स हैं।
Rails Cache Increment और Decrement
Rails.cache ऑब्जेक्ट में कैश्ड डेटा पर सीधे कार्य करने के लिए increment और decrement दोनों मेथड्स हैं जैसे हमारा काउंटर परिदृश्य:
threads = []
# प्रारंभिक काउंटर सेट करें
Rails.cache.write(:test_counter, 0, raw: true)
4.times do
threads << Thread.new do
100.times do
Rails.cache.increment(:test_counter)
# थ्रेड सेफ्टी को हाइलाइट करने के लिए सिर्फ इंक्रीमेंट दोहरा रहे हैं
Rails.cache.decrement(:test_counter)
Rails.cache.increment(:test_counter)
end
end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter, raw: true)
increment और decrement का उपयोग करने के लिए, हमें कैश स्टोर को बताना होगा कि यह एक 'raw' मान है (raw: true के माध्यम से)। आपको मान को वापस पढ़ते समय भी ऐसा करना होगा; अन्यथा, आपको एक त्रुटि मिलेगी। मूल रूप से, हम कैश को बता रहे हैं कि हम इस मान को एक नंगे पूर्णांक के रूप में स्टोर करना चाहते हैं ताकि हम इस पर इंक्रीमेंट/डिक्रीमेंट कॉल कर सकें, लेकिन आप अभी भी expires_in और अन्य कैश फ्लैग्स का उपयोग एक ही समय में कर सकते हैं।
यहाँ कुंजी यह है कि increment और decrement एटॉमिक ऑपरेशन्स का उपयोग करते हैं (कम से कम Redis और MemCache के लिए), जिसका अर्थ है कि वे थ्रेड-सेफ हैं; एक थ्रेड के लिए एटॉमिक ऑपरेशन के दौरान रुकने का कोई तरीका नहीं है।
यह ध्यान देने योग्य है, हालांकि मैंने इसे यहाँ अपने उदाहरण में उपयोग नहीं किया है, कि दोनों मेथड्स नया मान भी लौटाते हैं। इसलिए, यदि आपको केवल इसे अपडेट करने के अलावा नए काउंटर मान का उपयोग करने की आवश्यकता है, तो आप अतिरिक्त read कॉल के बिना भी ऐसा कर सकते हैं।
वास्तविक-विश्व एप्लिकेशन
सतह पर, ये increment और decrement मेथड्स लो-लेवल हेल्पर मेथड्स की तरह लगते हैं, जिस तरह की आप शायद केवल परवाह करते हैं यदि आप बैकग्राउंड जॉब प्रोसेसिंग जेम जैसी किसी चीज़ को लागू या बनाए रख रहे हैं। एक बार जब आप उनके बारे में जान जाते हैं, तो आप आश्चर्यचकित हो सकते हैं कि वे कहाँ काम आ सकते हैं।
मैंने इन्हें डुप्लिकेट शेड्यूल्ड बैकग्राउंड जॉब्स को एक साथ चलने से रोकने के लिए एक प्रोडक्शन एप्लिकेशन में उपयोग किया है। हमारे मामले में, हमारे पास खोज सूचकांक अपडेट करने, परित्यक्त कार्ट चिह्नित करने, आदि के लिए विभिन्न शेड्यूल्ड जॉब्स हैं। आम तौर पर, यह ठीक काम करता है; पकड़ यह है कि कुछ जॉब्स (विशेष रूप से खोज सूचकांक) बहुत अधिक मेमोरी का उपभोग करते हैं - इतना कि, यदि दो एक साथ चलते हैं, तो यह हमारे Heroku डायनो की सीमा से अधिक हो जाएगा, और वर्कर को मार दिया जाएगा। क्योंकि हमारे पास इनमें से कुछ जॉब्स हैं, यह उतना सरल नहीं है जितना उन्हें नो-रिट्रीज़ मार्क करना या यूनिक जॉब्स को मजबूर करना; दो अलग (और इस प्रकार यूनिक) जॉब्स एक ही समय में चलने का प्रयास कर सकते हैं और वर्कर को नीचे ला सकते हैं।
इसे रोकने के लिए, हमने शेड्यूल्ड जॉब्स के लिए एक बेस क्लास बनाई जो वर्तमान में चल रहे कितने हैं, इसका काउंटर रखती है। यदि काउंट बहुत अधिक है, तो जॉब बस खुद को फिर से एनक्यू करता है और इंतजार करता है।
मेरे एक साइड-प्रोजेक्ट पर एक और उदाहरण था जहाँ एक बैकग्राउंड जॉब (या एकाधिक जॉब्स) कुछ प्रोसेसिंग करते हैं जबकि उपयोगकर्ता को इंतजार करना पड़ता है। यह बैकग्राउंड जॉब के उपयोगकर्ता को वर्तमान प्रगति संप्रेषित करने के सामान्य मुद्दे को उठाता है। जबकि इसे हल करने के कई तरीके हैं, एक प्रयोग के रूप में, मैंने Rails.cache.increment का उपयोग करके ग्लोबली उपलब्ध काउंटर को अपडेट करने का प्रयास किया। संरचना इस प्रकार थी:
- सबसे पहले, मैंने
/app/modelsमें एक नई क्लास जोड़ी ताकि इस तथ्य को छुपाया जा सके कि काउंटर कैश में रहता था। यहीं सभी मान तक पहुंच प्रवाहित होगी। इसका हिस्सा जॉब से संबंधित अद्वितीय कैश कुंजी उत्पन्न करना है। - जॉब तब इस मॉडल का एक इंस्टेंस बनाता है और आइटम्स प्रोसेस होने पर इसे अपडेट करता है।
- एक सरल JSON एंडपॉइंट इस मॉडल का एक इंस्टेंस बनाकर वर्तमान मान प्राप्त करता है।
- फ्रंट-एंड हर कुछ सेकंड में इस एंडपॉइंट को पोल करता है ताकि UI अपडेट हो सके। आप निश्चित रूप से, ActionCable जैसी किसी चीज़ के साथ इसे फैंसी बना सकते हैं और अपडेट पुश कर सकते हैं।
निष्कर्ष
ईमानदारी से, Rails.cache.increment एक ऐसा टूल नहीं है जिसका मैं अक्सर उपयोग करूंगा, क्योंकि यह अक्सर नहीं होता कि मैं कैश में स्टोर डेटा को अपडेट करना चाहूं (जो स्वभाव से, कुछ हद तक अस्थायी है)। जैसा कि ऊपर उल्लेख किया गया है, जिन बार मैं इसके लिए पहुँचता हूँ वे आम तौर पर बैकग्राउंड जॉब्स से संबंधित होते हैं क्योंकि जॉब्स पहले से ही अपना डेटा Redis में स्टोर कर रहे हैं (कम से कम जिन ऐप्स पर मैंने काम किया है) और आम तौर पर अस्थायी भी हैं। ऐसे मामलों में, संबंधित डेटा (उदाहरण के लिए प्रतिशत पूर्ण) को उसी स्थान पर समान स्तर की अल्पकालिक दृढ़ता के साथ स्टोर करना स्वाभाविक लगता है।
सभी चीजों के साथ "बीटन पाथ से दूर", आपको अपने कोडबेस में इस तरह की चीजों को पेश करने से सावधान रहना चाहिए। मैं कम से कम, कुछ कमेंट्स जोड़ने की सिफारिश करूंगा ताकि भविष्य के डेवलपर्स को समझाया जा सके कि आप इस मेथड का उपयोग क्यों कर रहे हैं जिससे वे शायद परिचित नहीं हैं।