Express route मध्ये इमेज रिसाईझिंग करणे म्हणजे आपत्तीला निमंत्रण देण्यासारखे आहे. वापरकर्ता दहा मेगाबाइटचा फोटो अपलोड करतो, तुमचा सर्व्हर पिक्सेलवर प्रक्रिया (crunching pixels) करू लागतो आणि तीस सेकंदांनंतर रिक्वेस्ट टाइमआउट होते. नेमकी हीच समस्या टाळण्यासाठी बॅकग्राउंड जॉब क्यूज (Background job queues) असतात. Node.js इकोसिस्टममध्ये, Redis द्वारे असिंक्रोनस (asynchronous) कामे हाताळण्यासाठी Bull आणि BullMQ हे दोन दिग्गज ठरले आहेत. त्यांच्या मूळ रचनेत साम्य असले तरी, त्यांची कार्यपद्धती (philosophy) आणि दैनंदिन वापर (ergonomics) यामध्ये मोठा फरक आहे. योग्य निवड करणे महत्त्वाचे आहे कारण नंतर बदल करणे म्हणजे केवळ एखादे पॅकेज अपडेट करणे इतके सोपे नसते.
सामायिक पाया
दोन्ही लायब्ररी Redis चा मुख्य आधार (backbone) म्हणून वापर करतात. Redis अॅटॉमिक ऑपरेशन्स (atomic operations), विलंबित कामांसाठी सॉर्टेड सेट्स (sorted sets) आणि इव्हेंट्ससाठी pub/sub हाताळते. जर तुम्ही कॅशिंग किंवा सेशन्ससाठी आधीच Redis वापरत असाल, तर जॉब क्यू जोडण्यासाठी नवीन इन्फ्रास्ट्रक्चरची गरज पडत नाही. Bull आणि BullMQ दोन्ही प्रायोरिटी (priorities), बॅकऑफसह रिट्राय (retries with backoff), कॉनकरन्सी कंट्रोल (concurrency controls) आणि रिपीटेबल जॉब्सना (repeatable jobs) सपोर्ट करतात. या साम्यामुळे निवड करणे सोपे होण्याऐवजी अधिक कठीण होते. तुम्ही केवळ फीचर्सच्या चेकलिस्टवर अवलंबून राहू शकत नाही. त्याऐवजी, प्रत्येक लायब्ररी तुमच्या कोडची रचना कशी करण्यास सांगते, याकडे तुम्हाला लक्ष द्यावे लागेल.
Bull: एक अनुभवी आणि सिद्ध झालेला पर्याय
Bull अनेक वर्षांपासून अस्तित्वात आहे आणि हजारो प्रोडक्शन ॲप्लिकेशन्समध्ये वापरले जाते. ते उत्तम काम करते. याचे API सर्व गोष्टींना एकाच Queue instance मध्ये गुंडाळून ठेवते. तुम्ही एकाच ऑब्जेक्टवर त्याचे इन्स्टन्स तयार करता, प्रोसेसिंग फंक्शन परिभाषित करता आणि इव्हेंट्ससाठी लक्ष ठेवता. जर तुम्ही जुन्या Node.js पॅटर्नमधून येत असाल, तर ही मोनोलिथिक (monolithic) रचना परिचित वाटेल. ज्या कोडबेसमध्ये async/await च्या व्यापक वापरापूर्वीचे पॅटर्न आहेत, तिथे Bull नैसर्गिकरित्या जुळते, कारण त्याची वाढ कॉलबॅक आणि सुरुवातीच्या Redis क्लायंट्ससोबत झाली आहे.
याचा तोटा म्हणजे 'टाईट कपलिंग' (tight coupling). जेव्हा तुमचा API सर्व्हर एखादा जॉब तयार करतो, तेव्हा तो त्याच Queue ऑब्जेक्टला इम्पोर्ट करतो ज्यामध्ये वर्कर लॉजिक (worker logic) असते. व्यवहारात याचा अर्थ असा की, तुमच्या वेब प्रोसेसमध्ये अशा डिपेंडन्सीज (dependencies) येतात ज्यांची प्रत्यक्षात गरज नसते. ही एखादी गंभीर चूक नाही, पण यामुळे क्लीन आर्किटेक्चरमध्ये अडथळा येतो. साध्या कामांसाठी तुम्हाला याची जाणीवही होणार नाही, परंतु डझनभर मॉड्यूल्स असलेल्या मोठ्या टीमसाठी ही अडचण वाढत जाते.
BullMQ: पूर्णपणे नव्याने केलेली उभारणी
BullMQ हा अधिकृत उत्तराधिकारी आहे. हे पहिल्या दिवसापासून TypeScript मध्ये पुन्हा लिहिले गेले आहे, त्यामुळे 'टाइप्स' (types) हे केवळ JavaScript सोर्सवर नंतर जोडलेले नसून त्याचा अविभाज्य भाग आहेत. याचे API जबाबदाऱ्या वेगवेगळ्या क्लासेसमध्ये विभागते. Queue जॉब्स जोडण्याचे काम करते. Worker त्यांची प्रक्रिया (processing) हाताळते. QueueEvents ऑब्झर्व्हेबिलिटी (observability) हाताळते. हे विभाजन आधुनिक डिस्ट्रिब्युटेड सिस्टम्स (distributed systems) प्रत्यक्षात कशा प्रकारे कार्य करतात, याचे प्रतिबिंब आहे. तुमच्या API पॉड्सना (pods) फक्त Queue क्लास आणि Redis कनेक्शनची गरज असते. तुमच्या वर्कर पॉड्स Worker क्लास इम्पोर्ट करतात. ही सीमा केवळ वैचारिक नसून प्रत्यक्ष (physical) आहे.
मोठ्या टीममध्ये या बदलाचा मोठा फायदा होतो. एखादा डेव्हलपर नवीन फीचर तैनात करताना, प्रोसेसर कोणत्या फाईलमध्ये आहे हे न जाणून घेता जॉब एनक्यू (enqueue) करू शकतो. कंपायलर रनटाइमऐवजी जॉब डेटा आणि हँडलर्समधील 'टाइप मिसमॅच' (type mismatches) सुरुवातीलाच पकडतो. आधुनिक Node.js मध्ये async/await API देखील नेटिव्ह वाटतो. तुम्हाला जुन्या पद्धतींशी (legacy conventions) संघर्ष करावा लागणार नाही.
जॉब फ्लो: जुगाड ते प्रथम श्रेणीचे घटक
मल्टी-स्टेप वर्कफ्लोमध्ये (multi-step workflows) या दोन्ही लायब्ररीमधील सर्वात मोठा फरक दिसून येतो.
समजा तुम्ही ई-कॉमर्स इनव्हॉइसिंग पाइपलाइन (invoicing pipeline) तयार करत आहात. ग्राहक खरेदी पूर्ण करतो. तुम्हाला इन्व्हेंटरी राखून ठेवणे, कार्ड चार्जेबल करणे, PDF तयार करणे आणि ईमेल पाठवणे आवश्यक आहे. Bull मध्ये, या पायऱ्यांची साखळी (chaining) तयार करणे म्हणजे मॅन्युअल हिशोब ठेवण्यासारखे आहे. कदाचित तुमचा एक प्रोसेसर Redis किंवा मोठ्या डेटा पेलोडद्वारे स्टेट पास करून पुढचा जॉब सुरू करेल. तुम्हाला पालक-अपत्य (parent-child) समन्वय स्वतः लिहावा लागेल. जोपर्यंत सर्व काही व्यवस्थित चालते तोपर्यंत ठीक आहे, पण समस्या सुरू झाल्यावर 'रिट्राय लॉजिक' (retry logic) गोंधळात टाकणारे होते. जर PDF स्टेप फेल झाली, तर पेमेंट रद्द करण्यासाठी तुम्हाला कस्टम 'कॉम्पेन्सेशन कोड' (compensation code) लिहावा लागेल, ज्यामध्ये चुका होण्याची शक्यता जास्त असते.
BullMQ मध्ये FlowProducer ची ओळख करून दिली आहे. तुम्ही जॉब्सची एक 'ट्री' (tree) तयार करता जिथे पालक (parents) आपोआप त्यांच्या अपत्यांची (children) वाट पाहतात. इनव्हॉइसिंगच्या उदाहरणात, तुम्ही finalize-order नावाचा एक रूट जॉब तयार करता ज्यामध्ये reserve-inventory, charge-payment आणि generate-pdf हे तीन चाईल्ड जॉब्स असतात. तुम्ही ईमेल नोटिफिकेशनला PDF जॉबचे चाईल्ड बनवू शकता. Redis ही ग्राफ स्ट्रक्चर (graph structure) साठवते. जेव्हा प्रत्येक डिपेंडन्सी यशस्वी होते, तेव्हाच पालक जॉब सक्रिय होतो. जर एक चाईल्ड फेल झाले, तर संपूर्ण शाखा थांबते. तुम्हाला पोलिंग लूप्स (polling loops) किंवा रिकर्सिव्ह जॉब स्पॉनर्स (recursive job spawners) लिहिण्याची गरज पडत नाही. हे केवळ 'सिंटॅक्टिक शुगर' (syntactic sugar) नाही, तर हे तुमच्या बिझनेस लॉजिकच्या मॉडेलिंगची पद्धत बदलून टाकते.
रेट लिमिटिंग: एक ढोबळ साधन विरुद्ध अचूक शस्त्र
दोन्ही लायब्ररी थ्रूपुट (throughput) नियंत्रित करू शकतात, परंतु त्यांच्या अचूकतेमध्ये (granularity) प्रचंड फरक आहे.
Bull प्रत्येक queue साठी rate limits लागू करते. जर तुम्ही एका queue मध्ये प्रति सेकंद शंभर jobs प्रोसेस करण्यासाठी मर्यादा सेट केली, तर ती मर्यादा queue मधील प्रत्येक job वर समान प्रमाणात लागू होते. हे homogeneous workloads साठी ठीक आहे. परंतु multitenant SaaS प्लॅटफॉर्ममध्ये हे काम करत नाही. कल्पना करा की एखादा 'noisy' ग्राहक एका shared queue मध्ये लाखो webhook deliveries टाकत आहे. Bull च्या queue-level मर्यादेमुळे, तुम्ही इतर सर्वांचा वेग कमी न करता फक्त त्या एका tenant चा वेग कमी करू शकत नाही. तुमचे पर्याय कठीण आहेत. एकतर प्रत्येक ग्राहकासाठी स्वतंत्र Redis queues तयार करा आणि त्यांचे dynamic पद्धतीने व्यवस्थापन करा, किंवा ही अन्यायकारक परिस्थिती स्वीकारून ठेवा.
BullMQ मध्ये group-based rate limiting ची सुविधा आहे. तुम्ही प्रत्येक job ला एका group key ने (सहसा tenant किंवा user ID) टॅग करता आणि प्रत्येक group साठी मर्यादा ठरवता. तीच queue सर्व tenants साठी jobs प्रोसेस करते, परंतु scheduler प्रत्येक group ला स्वतंत्रपणे नियंत्रित (throttle) करतो. Customer A कडून येणारा अचानक वाढलेला लोड Customer B ला बाधित करत नाही. यामुळे queue ची संख्या अनावश्यकपणे वाढत नाही आणि तुमचा Redis keyspace सुव्यवस्थित राहतो. ज्या प्लॅटफॉर्म्सना 'noisy-neighbor' च्या समस्या आहेत, त्यांच्यासाठी केवळ ही एक गोष्ट migration करण्यासाठी पुरेशी आहे.
प्रत्यक्ष वापरात अधिक सुटसुटीत आर्किटेक्चर (Cleaner Architecture in Practice)
Queue आणि Worker मधील वेगळेपण उत्पादन (production) दरम्यान एखादी समस्या येईपर्यंत लक्षात येत नाही. Bull मध्ये, route handlers च्या खोलवर job creation code पाहणे सामान्य आहे, जे अनेकदा जड (heavy) processing dependencies देखील import करतात. BullMQ तुम्हाला काम नेमके कुठे होणार आहे हे ठरवण्यास भाग पाडते. तुमचे web servers हलके (lean) राहतात. तुमचे worker containers जड libraries, image processors किंवा headless browsers एकत्रितपणे हाताळतात. जर memory leak आढळला, तर तुम्हाला नेमका कोणता process type profile करायचा आहे हे स्पष्टपणे समजते. याचे mental model Celery किंवा Sidekiq सारख्या सिस्टिम्सच्या जवळ जाणारे आहे.
निवड कशी करावी (Making the Choice)
जर तुम्ही नवीन प्रोजेक्ट (greenfield project) सुरू करत असाल, तर BullMQ ने सुरुवात करा. TypeScript definitions अचूक आणि पूर्ण आहेत. Job flows मुळे orchestration code चा मोठा डोंगर कमी होतो. Group rate limiting मुळे समस्या निर्माण होण्यापूर्वीच त्या सुटतात. async/await API वापरणे नैसर्गिक वाटते. नवीन प्रोजेक्टसाठी जुनी library निवडण्याचे फारसे कारण नाही.
जर Bull आधीच व्यवस्थित काम करत असेल, तर तेच वापरा. Migration साठी वेळ लागतो आणि स्थिरतेचा (stability) धोका असतो. जर तुमचे jobs साधे आणि स्वतंत्र असतील, तर तुम्हाला आवश्यक असलेल्या फीचर्सची कमतरता भासणार नाही. पासवर्ड रिसेट ईमेल पाठवणारी आणि avatars रिसाईज करणारी queue साठी flow graphs ची गरज नसते. केवळ सैद्धांतिक शुद्धतेसाठी (theoretical purity) चालू असलेला कोड पुन्हा लिहिणे म्हणजे इंजिनिअरिंग नाही, तर तो केवळ छंद (hobbyism) आहे.
Migration ची वास्तव स्थिती (Migration Reality Check)
जर तुम्ही बदल करण्याचा निर्णय घेतला, तर त्याला कोड refactor म्हणून न बघता पायाभूत सुविधांमधील (infrastructure) बदल म्हणून पहा. Bull आणि BullMQ वेगवेगळ्या Redis key schemas वापरतात. ते एकमेकांचा job data किंवा state वाचू शकत नाहीत. तुम्ही फक्त एक feature flag बदलून जुने jobs पूर्ण होण्याची अपेक्षा करू शकत नाही. तुम्हाला प्रत्येक अस्तित्वात असलेली queue पूर्णपणे रिकामी करावी लागेल, नवीन workers deploy करावे लागतील आणि BullMQ सह enqueueing सुरू करावे लागेल. यासाठी maintenance window किंवा blue-green deployment चे नियोजन करा, जिथे जुने workers जुनी queue हाताळतील आणि नवीन workers नवीन queue हाताळतील.
