संभावना है, यदि आप Ruby कोड लिख रहे हैं, तो आप पृष्ठभूमि प्रसंस्करण को संभालने के लिए Sidekiq का उपयोग कर रहे हैं। यदि आप ActiveJob या किसी अन्य पृष्ठभूमि से आ रहे हैं, तो बने रहें, कवर किए गए कुछ सुझाव वहां भी लागू किए जा सकते हैं।
लोग विभिन्न मामलों के लिए (Sidekiq) पृष्ठभूमि जॉब का उपयोग करते हैं। कुछ संख्याओं को क्रंच करते हैं, कुछ उपयोगकर्ताओं को स्वागत ईमेल भेजते हैं, और कुछ डेटा सिंकिंग शेड्यूल करते हैं। आपका मामला जो भी हो, आप अंततः डुप्लीकेट जॉब से बचने की आवश्यकता में पड़ सकते हैं। डुप्लीकेट जॉब से मेरा मतलब दो जॉब से है जो बिल्कुल एक ही काम करते हैं। आइए उस पर थोड़ा गहराई से जाएं।
जॉब को डी-डुप्लीकेट क्यों करें?
एक ऐसे परिदृश्य की कल्पना करें जहां आपका जॉब निम्नलिखित जैसा दिखता है:
class BookSalesWorker include Sidekiq::Worker def perform(book_id) crunch_some_numbers(book_id) upload_to_s3 end ... end
BookSalesWorker हमेशा एक ही काम करता है — book_id के आधार पर एक किताब के लिए DB को क्वेरी करता है और कुछ संख्याओं की गणना करने के लिए नवीनतम बिक्री डेटा प्राप्त करता है। फिर, यह उन्हें एक स्टोरेज सेवा पर अपलोड करता है। ध्यान रखें कि हर बार जब आपकी वेबसाइट पर एक किताब बेची जाती है, तो आपके पास यह जॉब एन्क्यू किया जाएगा।
अब, क्या होगा यदि आपको एक साथ 100 बिक्री मिली? आपके पास ये 100 जॉब होंगे जो बिल्कुल एक ही काम कर रहे हैं। शायद आप इससे ठीक हैं। आपको S3 राइट्स की इतनी परवाह नहीं है, और आपकी कतारें उतनी भीड़भाड़ वाली नहीं हैं, इसलिए आप लोड को संभाल सकते हैं। लेकिन, "क्या यह स्केल करता है?"™️
खैर, निश्चित रूप से नहीं। यदि आप अधिक किताबों के लिए अधिक बिक्री प्राप्त करना शुरू करते हैं, तो आपकी कतार अनावश्यक काम से जल्दी भर जाएगी। यदि आपके पास एक किताब के लिए एक ही काम करने वाले 100 जॉब हैं, और आपके पास 10 किताबें समानांतर में बिक रही हैं, तो आप वास्तव में प्रत्येक किताब के लिए केवल 10 जॉब रखने के बजाय अपनी कतार में 1000 जॉब गहरे हैं।
अब, आइए कुछ विकल्पों पर जाएं कि आप डुप्लीकेट जॉब को अपनी कतारों को भरने से कैसे रोक सकते हैं।
1. DIY तरीका
यदि आप बाहरी निर्भरता और जटिल तर्क के प्रशंसक नहीं हैं, तो आप आगे बढ़ सकते हैं और अपने कोडबेस में कुछ कस्टम समाधान जोड़ सकते हैं। मैंने हमारे उदाहरणों को पहले हाथ से आजमाने के लिए एक नमूना रेपो बनाया। प्रत्येक दृष्टिकोण में एक लिंक होगा।
1.1 एक फ्लैग दृष्टिकोण
आप एक फ्लैग जोड़ सकते हैं जो तय करता है कि जॉब को एन्क्यू करना है या नहीं। कोई अपनी Book टेबल में sales_enqueued_at जोड़ सकता है और उसे बनाए रख सकता है। उदाहरण के लिए:
module BookSalesService def schedule_with_one_flag(book) # जांचें कि क्या जॉब 10 मिनट से अधिक पहले एन्क्यू किया गया था if book.sales_enqueued_at < 10.minutes.ago book.update(sales_enqueued_at: Time.current) BookSalesWorker.perform_async(book.id) end end end
इसका मतलब है कि अंतिम जॉब के एन्क्यू होने के समय से 10 मिनट बीतने तक कोई नया जॉब एन्क्यू नहीं किया जाएगा। 10 मिनट बीत जाने के बाद, हम फिर sales_enqueued_at को अपडेट करते हैं और एक नया जॉब एन्क्यू करते हैं।
एक और चीज जो आप कर सकते हैं वह है एक फ्लैग सेट करना जो बूलियन है, जैसे crunching_sales। आप पहली जॉब एन्क्यू होने से पहले crunching_sales को true पर सेट करते हैं। फिर, आप इसे false पर सेट करते हैं एक बार जॉब पूरा हो जाने पर। अन्य सभी जॉब जो शेड्यूल होने का प्रयास करते हैं, तब तक अस्वीकार कर दिए जाएंगे जब तक crunching_sales false नहीं हो जाता।
आप इस दृष्टिकोण को मेरे द्वारा बनाए गए उदाहरण रेपो में आजमा सकते हैं।
1.2 दो फ्लैग दृष्टिकोण
यदि जॉब को 10 मिनट के लिए एन्क्यू होने से "लॉक" करना बहुत डरावना लगता है, लेकिन आप अभी भी अपने कोड में अतिरिक्त फ्लैग के साथ ठीक हैं, तो अगला सुझाव आपको रुचिकर लग सकता है।
आप मौजूदा sales_enqueued_at में एक और फ्लैग जोड़ सकते हैं — sales_calculated_at। तब हमारा कोड कुछ इस तरह दिखेगा:
module BookSalesService def schedule_with_two_flags(book) # जांचें कि क्या बिक्री अभी गणना की जा रही है if book.sales_enqueued_at <= book.sales_calculated_at book.update(sales_enqueued_at: Time.current) BookSalesWorker.perform_async(book.id) end end end class BookSalesWorker include Sidekiq::Worker def perform(book_id) crunch_some_numbers(book_id) upload_to_s3 # नया जोड़ book.update(sales_calculated_at: Time.current) end ... end
इसे आजमाने के लिए, उदाहरण रेपो में निर्देशों की जांच करें।
अब हम उस समय के एक हिस्से को नियंत्रित करते हैं जब एक जॉब एन्क्यू किया जाता है और समाप्त होता है। उस समय के हिस्से में, कोई जॉब एन्क्यू नहीं किया जा सकता। जब जॉब चल रहा होता है, तो sales_enqueued_at sales_calculated_at से बड़ा होगा। जब जॉब चलना समाप्त होता है, तो sales_calculated_at अधिक हालिया (बड़ा) होगा sales_enqueued_at से और एक नया जॉब एन्क्यू हो जाएगा।
दो फ्लैग का उपयोग करना दिलचस्प हो सकता है, ताकि आप UI में दिखा सकें कि उन बिक्री संख्याओं को आखिरी बार कब अपडेट किया गया था। तब उपयोगकर्ता जो उन्हें पढ़ते हैं, उन्हें इस बात का अंदाजा हो सकता है कि डेटा कितना हालिया है। एक जीत-जीत स्थिति।
फ्लैग सारांश
जरूरत के समय में ऐसे समाधान बनाना आकर्षक हो सकता है, लेकिन मुझे वे थोड़े अनाड़ी लगते हैं, और वे कुछ ओवरहेड जोड़ते हैं। मैं इसका उपयोग करने की सलाह दूंगा यदि आपका उपयोग मामला सरल है, लेकिन जैसे ही यह जटिल या पर्याप्त नहीं साबित होता, मैं आपसे अन्य विकल्पों को आजमाने का आग्रह करूंगा।
फ्लैग दृष्टिकोण के साथ एक बड़ा नुकसान यह है कि आप उन सभी जॉब को खो देंगे जिन्होंने उन 10 मिनट के दौरान एन्क्यू करने का प्रयास किया। एक बड़ा फायदा यह है कि आप निर्भरता नहीं ला रहे हैं, और यह कतारों में जॉब की संख्या को बहुत जल्दी कम कर देगा।
1.3 कतार को पार करना
एक और दृष्टिकोण जो आप अपना सकते हैं वह है एक कस्टम लॉकिंग तंत्र बनाना ताकि वही जॉब एन्क्यू न हों। हम उस Sidekiq कतार की जांच करेंगे जिसमें हम रुचि रखते हैं और देखेंगे कि क्या जॉब (वर्कर) पहले से ही वहां है। कोड कुछ इस तरह दिखेगा:
module BookSalesService def schedule_unique_across_queue(book) queue = Sidekiq::Queue.new('default') queue.each do |job| return if job.klass == BookSalesWorker.to_s && job.args == [book.id] end BookSalesWorker.perform_async(book.id) end end class BookSalesWorker include Sidekiq::Worker def perform(book_id) crunch_some_numbers(book_id) upload_to_s3 end ... end
ऊपर दिए गए उदाहरण में हम जांच रहे हैं कि क्या 'default' कतार में BookSalesWorker वर्ग नाम वाला जॉब है। हम यह भी जांच रहे हैं कि क्या जॉब आर्गुमेंट्स बुक आईडी से मेल खाते हैं। यदि BookSalesWorker जॉब उसी बुक आईडी के साथ कतार में है, तो हम जल्दी वापस लौटेंगे और दूसरा शेड्यूल नहीं करेंगे।
ध्यान दें कि उनमें से कुछ शेड्यूल हो सकते हैं यदि आप जॉब को बहुत तेजी से शेड्यूल करते हैं क्योंकि कतार खाली है। जब मैंने इसे स्थानीय रूप से टेस्ट किया तो मेरे साथ ठीक यही हुआ:
100.times { BookSalesService.schedule_unique_across_queue(book) }
आप इसे उदाहरण रेपो में आजमा सकते हैं।
इस दृष्टिकोण के बारे में अच्छी बात यह है कि यदि आपको आवश्यकता हो तो आप सभी कतारों को पार करके मौजूदा जॉब की खोज कर सकते हैं। नुकसान यह है कि यदि आपकी कतार खाली है और आप एक साथ बहुत सारे शेड्यूल करते हैं तो आपके पास अभी भी डुप्लीकेट जॉब हो सकते हैं। साथ ही, आप एक जॉब शेड्यूल करने से पहले कतार में सभी जॉब को पार कर रहे हैं, इसलिए यह आपकी कतार के आकार के आधार पर महंगा हो सकता है।
2. Sidekiq Enterprise में अपग्रेड करना
यदि आपके या आपके संगठन के पास कुछ पैसा पड़ा है, तो आप Sidekiq के एंटरप्राइज संस्करण में अपग्रेड कर सकते हैं। यह $179 प्रति माह से शुरू होता है, और इसमें एक अच्छी सुविधा है जो आपको डुप्लीकेट जॉब से बचने में मदद करती है। दुर्भाग्य से, मेरे पास Sidekiq Enterprise नहीं है, लेकिन मेरा मानना है कि उनका दस्तावेज़ीकरण पर्याप्त है। आप निम्न कोड के साथ आसानी से अद्वितीय (गैर-डुप्लीकेट) जॉब प्राप्त कर सकते हैं:
class BookSalesWorker include Sidekiq::Worker sidekiq_options unique_for: 10.minutes def perform(book_id) crunch_some_numbers(book_id) upload_to_s3 end ... end
और बस। आपके पास 'वन फ्लैग दृष्टिकोण' अनुभाग में वर्णित के समान जॉब कार्यान्वयन है। जॉब 10 मिनट के लिए अद्वितीय होगा, जिसका अर्थ है कि उस समय अवधि में समान आर्गुमेंट वाला कोई अन्य जॉब शेड्यूल नहीं किया जा सकता।
बहुत अच्छा वन-लाइनर, है ना? खैर, यदि आपके पास Enterprise Sidekiq है और आपने अभी इस सुविधा के बारे में पता लगाया है, तो मुझे वास्तव में खुशी है कि मैंने मदद की। हम में से अधिकांश इसका उपयोग नहीं करने जा रहे हैं, तो आइए अगले समाधान पर जाएं।
3. sidekiq-unique-jobs बचाव के लिए
हां, मुझे पता है कि हम एक जेम का उल्लेख करने वाले हैं। और हां, इसमें कुछ Lua फाइलें हैं जो कुछ लोगों को परेशान कर सकती हैं। लेकिन मेरे साथ बने रहें, यह वास्तव में एक मीठा सौदा है जो आपको इसके साथ मिल रहा है। sidekiq-unique-job जेम बहुत सारे लॉकिंग और अन्य कॉन्फ़िगरेशन विकल्पों के साथ आता है — शायद आपकी आवश्यकता से अधिक।
तेजी से शुरू करने के लिए, अपने Gemfile में sidekiq-unique-jobs जेम डालें, bundle करें और अपने वर्कर को दिखाए अनुसार कॉन्फ़िगर करें:
class UniqueBookSalesWorker include Sidekiq::Worker sidekiq_options lock: :until_executed, on_conflict: :reject def perform(book_id) book = Book.find(book_id) logger.info "I am a Sidekiq Book Sales worker - I started" sleep 2 logger.info "I am a Sidekiq Book Sales worker - I finished" book.update(sales_calculated_at: Time.current) book.update(crunching_sales: false) end end
बहुत सारे विकल्प हैं, लेकिन मैंने सरल बनाने और इस एक का उपयोग करने का फैसला किया:
sidekiq_options lock: :until_executed, on_conflict: :reject
lock: :until_executed पहले UniqueBookSalesWorker जॉब को तब तक लॉक करेगा जब तक यह निष्पादित नहीं हो जाता। on_conflict: :reject के साथ, हम कह रहे हैं कि हम चाहते हैं कि निष्पादित होने का प्रयास करने वाले अन्य सभी जॉब को डेड कतार में अस्वीकार कर दिया जाए। हमने यहां जो हासिल किया है वह हमारे DIY उदाहरणों में किए गए के समान है।
उन DIY उदाहरणों पर एक मामूली सुधार यह है कि हमारे पास एक तरह का लॉग है कि क्या हुआ। यह समझने के लिए कि यह कैसा दिखता है, आइए निम्नलिखित प्रयास करें:
5.times { UniqueBookSalesWorker.perform_async(Book.last.id) }
केवल एक जॉब पूरी तरह से निष्पादित होगा, और अन्य चार जॉब डेड कतार में भेज दिए जाएंगे, जहां आप उन्हें पुनः प्रयास कर सकते हैं। यह दृष्टिकोण हमारे उदाहरणों से भिन्न है जहां डुप्लीकेट जॉब को बस अनदेखा कर दिया गया था।
जब लॉकिंग और संघर्ष समाधान की बात आती है तो चुनने के लिए बहुत सारे विकल्प हैं। मैं सुझाव देता हूं कि आप अपने विशिष्ट उपयोग मामले के लिए जेम के दस्तावेज़ीकरण से परामर्श लें।
महान अंतर्दृष्टि
इस जेम के बारे में महान बात यह है कि आप अपने कतारों में क्या हुआ इसके लॉक और इतिहास देख सकते हैं। आपको बस अपने config/routes.rb में निम्नलिखित पंक्तियां जोड़नी हैं:
# config/routes.rb require 'sidekiq_unique_jobs/web' Rails.application.routes.draw do mount Sidekiq::Web, at: '/sidekiq' end
यह मूल Sidekiq क्लाइंट को शामिल करेगा, लेकिन यह आपको दो और पेज भी देगा — एक जॉब लॉक के लिए और दूसरा चेंजलॉग के लिए। यह ऐसा दिखता है:
ध्यान दें कि हमारे पास दो नए पेज हैं, "लॉक्स" और "चेंजलॉग्स"। बहुत अच्छी सुविधा।
आप यह सब उदाहरण प्रोजेक्ट में आजमा सकते हैं जहां जेम इंस्टॉल है और जाने के लिए तैयार है।
Lua क्यों?
सबसे पहले, मैं जेम का लेखक नहीं हूं, इसलिए मैं यहां सिर्फ अनुमान लगा रहा हूं। जब मैंने पहली बार जेम देखा, तो मुझे आश्चर्य हुआ: Ruby जेम के अंदर Lua का उपयोग क्यों करें? यह पहली नज़र में अजीब लग सकता है, लेकिन Redis Lua स्क्रिप्ट चलाने का समर्थन करता है। मुझे लगता है कि जेम के लेखक के मन में यह था और वे Lua में अधिक फुर्तीला तर्क करना चाहते थे।
यदि आप जेम के रेपो में Lua फाइलों को देखते हैं, वे उतने जटिल नहीं हैं। सभी Lua स्क्रिप्ट को बाद में Ruby कोड में SidekiqUniqueJobs::Script::Caller से बुलाया जाता है। कृपया स्रोत कोड देखें, यह पढ़ने और यह पता लगाने के लिए दिलचस्प है कि चीजें कैसे काम करती हैं।
वैकल्पिक जेम
यदि आप ActiveJob का बड़े पैमाने पर उपयोग करते हैं, तो आप active-job-uniqueness जेम को यहीं आजमा सकते हैं। विचार समान है, लेकिन कस्टम Lua स्क्रिप्ट के बजाय, यह [Redlock] का उपयोग Redis में आइटम लॉक करने के लिए करता है।
इस जेम का उपयोग करके एक अद्वितीय जॉब प्राप्त करने के लिए, आप इस तरह के जॉब की कल्पना कर सकते हैं:
class BookSalesJob < ActiveJob::Base unique :until_executed def perform ... end end
सिंटैक्स कम वर्बोज है लेकिन sidekiq-unique-jobs जेम के समान ही है। यह आपके मामले को हल कर सकता है यदि आप ActiveJob पर बहुत अधिक भरोसा करते हैं।
अंतिम विचार
मुझे आशा है कि आपको अपने ऐप में डुप्लीकेट जॉब से निपटने के बारे में कुछ ज्ञान प्राप्त हुआ। मुझे निश्चित रूप से विभिन्न समाधानों पर शोध करने और उनके साथ खेलने में मज़ा आया। यदि आपको वह नहीं मिला जो आप ढूंढ रहे थे, तो मुझे आशा है कि कुछ उदाहरणों ने आपको अपना कुछ बनाने के लिए प्रेरित किया।
यहां सभी कोड स्निपेट्स के साथ उदाहरण प्रोजेक्ट है।
अगले में मिलते हैं, चीयर्स।
P.S. यदि आप Ruby Magic पोस्ट को प्रेस से निकलते ही पढ़ना चाहते हैं, तो हमारे Ruby Magic न्यूज़लेटर की सदस्यता लें और कभी भी एक भी पोस्ट मिस न करें!