जेव्हा तुम्ही अझरबैजानमध्ये कार ब्रोकरेज चालवता आणि युनायटेड स्टेट्समधून साल्वेज वाहने (salvage vehicles) आयात करता, तेव्हा तुमच्या सॉफ्टवेअरच्या समस्या सिलिकॉन व्हॅली स्टार्टअपच्या समस्यांपेक्षा वेगळ्या असतात. तुम्ही लाखो एकाच वेळी वापरणाऱ्या युजर्ससाठी (concurrent users) ऑप्टिमाइझ करत नाही. तुम्ही स्पष्टता, अपटाइम (uptime) आणि रात्रीच्या वेळी बारा तासांच्या फरकाने असलेल्या ऑक्शन हाऊससोबत समन्वय साधत असताना स्वतः गोष्टी दुरुस्त करण्याच्या क्षमतेसाठी ऑप्टिमाइझ करत असता. जेव्हा मी AutoMakler बनवले, तेव्हा मी अगदी याच परिस्थितीत होतो. हे प्लॅटफॉर्म लाईव्ह ऑक्शन स्क्रॅपिंग आणि Carfax लूकअपपासून ते डिलिव्हरी एस्टिमेट आणि पेमेंट प्रोसेसिंगपर्यंत सर्व काही हाताळते. हे खऱ्या ग्राहकांसाठी सेवा देणारी एक वास्तविक प्रोडक्शन सिस्टिम आहे आणि ती अशा गोष्टींवर चालते ज्याला बहुतेक डेव्हलपर्स 'अत्यंत बोअरिंग स्टॅक' (aggressively boring stack) म्हणतील.
असा स्टॅक ज्याबद्दल कोणीही सांगायला आवडणार नाही
येथे React नाही. Vue नाही. Redis नाही, Celery नाही आणि WebSocket सर्व्हरही नाही. बॅकएंड FastAPI आणि साधे Python आहे. डेटाबेस PostgreSQL आहे. फ्रंटएंड हे Jinja2 टेम्पलेट्स, Bootstrap आणि थोड्या प्रमाणात vanilla JavaScript वापरून रेंडर केलेले HTML आहे. स्क्रॅपिंगसाठी मी Playwright वापरतो. सर्व काही एका सिंगल Python प्रोसेसमध्ये चालते जी थेट HTML सर्व्ह करते.
येथे कोणताही बिल्ड स्टेप (build step) नाही. ऑडिट करण्यासाठी कोणतेही node_modules फोल्डर्स नाहीत, कॉन्फिगर करण्यासाठी कोणतेही ट्रान्सपायलर (transpilers) नाहीत आणि अपडेट्ससोबत ताळमेळ राखण्यासाठी कोणतेही फ्रंटएंड फ्रेमवर्क चर्न (frontend framework churn) नाही. जेव्हा मी डिप्लॉय करतो, तेव्हा मी फक्त Python फाइल्स आणि टेम्पलेट्स हलवतो, बंडलरची (bundlers) एखादी पाइपलाइन ऑर्केस्ट्रेट करत नाही. ही साधेपणा कोणतीही तडजोड नाही. तर तोच मुख्य उद्देश आहे.
मेसेज ब्रोकरशिवाय जॉब्स कशा रांगेत (Queue) लावायचे
लाईव्ह कार ऑक्शनचे स्क्रॅपिंग सिंक्रोनस पद्धतीने (synchronously) होऊ शकत नाही. Playwright ने पेज लोड करणे, JavaScript एक्झिक्युट करणे आणि डेटा काढणे या प्रक्रियेत एक सिंगल स्क्रॅप करण्यासाठी काही सेकंद लागू शकतात. ही प्रक्रिया सुरू असताना युजरला थांबवून ठेवणे हा पर्याय नाही. स्टँडर्ड पद्धत सांगते की Redis इन्स्टॉल करा, Celery कॉन्फिगर करा आणि वर्कर पूल (worker pool) तयार करा. मी या सर्वांना वगळले.
त्याऐवजी, AutoMakler स्वतःच्या जॉब क्यू (job queue) म्हणून Postgres वापरते. जेव्हा एखादा युजर स्क्रॅप ट्रिगर करतो, तेव्हा ॲप्लिकेशन 'pending' स्टेटससह 'tasks' टेबलमध्ये एक नवीन रो (row) लिहितो. एक asyncio बॅकग्राउंड टास्क ती रो घेतो आणि ब्राउझर स्क्रॅपिंग सुरू करतो. दरम्यान, ब्राउझर स्टेटस तपासण्यासाठी दर तीन सेकंदांनी एका हलक्या (lightweight) एंडपॉइंटला पोल (poll) करतो. जेव्हा रो 'completed' मध्ये अपडेट होते, तेव्हा पेज रिफ्रेश होते आणि रिझल्ट्स दिसतात.
ही पद्धत काम करते कारण पोलिंग इंटरव्हल (polling interval) इतका कमी आहे की तो रिस्पॉन्सिव्ह वाटतो, पण सर्व्हरवर ताण येईल इतका मोठा नाही. संगणकासाठी तीन सेकंद खूप मोठा काळ आहे आणि बाहेरील ऑक्शन साइटची वाट पाहणाऱ्या मानवासाठी तो फारसा जाणवत नाही. डेटाबेस कॉनकरन्सी (concurrency) नॅटिव्हली हाताळतो, आणि जॉब्स केवळ Postgres मधील रो असल्याने, मी Celery लॉग्स किंवा Redis कीज शोधण्याऐवजी साध्या SQL क्वेरीने क्यू (queue) तपासू शकतो.
वर्कर पूलशिवाय सर्व्हर जिवंत कसा ठेवायचा
ब्राउझर ऑटोमेशनला मेमरीची खूप गरज असते. एकाच वेळी खूप जास्त Playwright इन्स्टन्स सुरू केले तर तुमचा सर्व्हर कोलमडून पडेल. पारंपारिक उपाय म्हणजे कॉनकरन्सी लिमिट्ससह एक मॅनेज्ड वर्कर पूल, जो अनेकदा त्याच Redis आणि Celery च्या कॉम्बिनेशनवर आधारित असतो. मी पायथनची एक ओळ वापरतो: asyncio.Semaphore.
सेमाफोर (semaphore) एकाच वेळी किती ब्राउझर इन्स्टन्स चालू शकतात यावर मर्यादा घालतो. जेव्हा नवीन स्क्रॅप रिक्वेस्ट येते, तेव्हा ती एकतर लगेच स्लॉट घेते किंवा एक स्लॉट मोकळा होईपर्यंत थांबते. हे सर्व एकाच प्रोसेसमध्ये घडते. अयशस्वी होण्यासाठी कोणताही बाह्य ऑर्केस्ट्रेटर नाही, शांतपणे बंद पडण्यासाठी कोणताही वर्कर प्रोसेस नाही आणि मॉनिटर करण्यासाठी कोणतेही अतिरिक्त इन्फ्रास्ट्रक्चर नाही. माझी मेमरी प्रेडिक्टेबल राहते आणि सर्व्हरचे संरक्षण करणारा कोड थेट तो वापरणाऱ्या कोडच्या शेजारी असतो, तो डिप्लॉयमेंट मॅनिफेस्टमध्ये लपलेला नसतो.
एकाच कॉलबॅक URL द्वारे पैसे ट्रान्सफर करणे
पेमेंट प्रोसेसिंगमुळे एक अशी मर्यादा आली जी मी बदलू शकत नव्हतो. माझे पेमेंट गेटवे प्रत्येक मर्चंट अकाउंटसाठी नेमके एक कॉलबॅक URL (callback URL) देते, परंतु मला त्या एकाच अकाउंटद्वारे दोन वेगवेगळ्या प्रोजेक्ट्ससाठी व्यवहार प्रक्रिया करायची होती. दुसरे मर्चंट प्रोफाइल तयार करणे म्हणजे अतिरिक्त शुल्क, अतिरिक्त कंप्लायन्स आणि अतिरिक्त कागदपत्रे, ज्यासाठी एका लहान ब्रोकरेजकडे वेळ नाही.
यावर उपाय म्हणजे ग्राहकाला गेटवेकडे पाठवण्यापूर्वी ऑर्डर आयडी (order ID) स्ट्रिंगमध्ये थेट प्रोजेक्टचे नाव एनकोड (encode) करणे. जेव्हा कॉलबॅक माझ्या सर्व्हरवर येतो, तेव्हा AutoMakler तो आयडी डिकोड करते, पेमेंट कोणत्या प्रोजेक्टचे आहे ते ओळखते आणि नोटिफिकेशन योग्य अंतर्गत हँडलरकडे पाठवते. अस्तित्वात असलेले लॉजिक तसेच राहिले. हे 'ॲडिटिव्ह डिझाइन' (additive design) आहे: मी पेमेंट फ्लो पुन्हा लिहिला नाही, मी फक्त आयडेंटिफायरला थोडा अधिक संदर्भ (context) दिला. हा असा प्रकारचा 'हॅक' आहे जो नंतर पाहताना अगदी स्पष्ट वाटतो, पण आर्किटेक्चरल कसरत वाचवून तासनतास वाचवतो.
WebSockets शिवाय चालणारे चॅट
कस्टमर सपोर्ट चॅटमध्ये सहसा इंजिनिअर्स तडजोड करतात आणि WebSockets जोडतात. मला इन-ॲप मेसेजिंग हवे होते, पण मला इन्फ्रास्ट्रक्चरचा वापर (footprint) देखील अत्यंत कमी ठेवायचा होता. म्हणून मी ऑक्शन स्क्रॅप्ससाठी (auction scrapes) वापरली जाणारी तीच पोलिंग स्ट्रॅटेजी (polling strategy) पुन्हा वापरली.
मेसेज Postgres मध्ये साठवले जातात. जेव्हा एखादा वापरकर्ता मेसेज पाठवतो, तेव्हा तो टेबलमध्ये लिहिला जातो. क्लायंट अपडेट्ससाठी पोलिंग करतो आणि UI मध्ये नवीन मेसेज आणि रीड रिसिप्ट्स (read receipts) जवळजवळ रिअल-टाइममध्ये दिसतात. संभाषणाचे टेबल जसजसे मोठे होईल, तरीही हे वेगवान ठेवण्यासाठी, मी एक Postgres partial index जोडला जो फक्त सक्रिय संभाषणातील न वाचलेल्या (unread) मेसेजसाठी काम करतो. डेटाबेस जुना इतिहास स्कॅन करण्यात सायकल (cycles) वाया घालवत नाही आणि क्वेरी प्लॅनर (query planner) एका मर्यादित इंडेक्स रेंज स्कॅनद्वारे बहुतेक चॅट लूकअप्स पूर्ण करू शकतो.
सपोर्ट चॅटसाठी जिथे काही सेकंदांचा लॅटन्सी (latency) स्वीकारार्ह आहे, तिथे हे पूर्णपणे पुरेसे आहे. वापरकर्त्यांना आवश्यक प्रतिसाद मिळतो आणि मला कधीही एखादे जुने (stale) WebSocket कनेक्शन डीबग करावे लागले नाही किंवा वेगळा सॉकेट सर्व्हर मॅनेज करावा लागला नाही.
प्रामाणिक तोटे
या आर्किटेक्चरमध्ये काही वास्तविक तडजोडी (trade-offs) आहेत आणि तसे नसून दाखवणे असत्य ठरेल. पोलिंगमुळे नेटवर्कवर सतत हालचाल (chatty) होते. दर तीन सेकंदांनी, प्रत्येक सक्रिय क्लायंट सर्व्हरला रिक्वेस्ट पाठवतो. बँडविड्थ आणि क्वेरी लोड हा पर्सिस्टंट सॉकेट कनेक्शनच्या तुलनेत जास्त असतो. जर Python प्रोसेस रीस्टार्ट झाली, तर चालू असलेले कोणतेही बॅकग्राउंड टास्क लगेच थांबते कारण ते पुन्हा सुरू करण्यासाठी कोणताही बाह्य वर्कर (external worker) उपलब्ध नसतो. मी हे स्वीकारले आहे कारण ही टास्क लहान आहेत आणि पुन्हा प्रयत्न करण्याचा (retry) खर्च कमी आहे. अयशस्वी झालेला ब्राउझर स्क्रॅप वापरकर्त्याद्वारे पुन्हा सहज सुरू केला जाऊ शकतो.
या दृष्टिकोनाला एक मर्यादा देखील आहे. जर AutoMakler ला कधी हजारो एकाच वेळी होणाऱ्या स्क्रॅप्सना सेवा द्यावी लागली, तर पोलिंगसह असलेले सिंगल-प्रोसेस मॉडेलवर ताण येईल. पण मी ज्या व्यवसायात आहे तसे ते नाही. मला डझनभर एकाच वेळी वापरणाऱ्या युजर्ससाठी विश्वासार्हता हवी आहे, नाही तर
