അസർബൈജാനിൽ ഒരു കാർ ബ്രോക്കറേജ് നടത്തുകയും അമേരിക്കയിൽ നിന്ന് സെൽവേജ് (salvage) വാഹനങ്ങൾ ഇറക്കുമതി ചെയ്യുകയും ചെയ്യുമ്പോൾ, നിങ്ങളുടെ സോഫ്റ്റ്വെയർ പ്രശ്നങ്ങൾ സിലിക്കൺ വാലിയിലെ സ്റ്റാർട്ടപ്പുകളിൽ നിന്ന് വ്യത്യസ്തമായിരിക്കും. ലക്ഷക്കണക്കിന് ആളുകൾ ഒരേസമയം ഉപയോഗിക്കുന്നതിനായല്ല നിങ്ങൾ ഇത് തയ്യാറാക്കുന്നത്. മറിച്ച്, വ്യക്തതയ്ക്കും (clarity), അപ്ടൈമിനും (uptime), പന്ത്രണ്ട് ടൈം സോണുകൾ അകലെയുള്ള ഒരു ഓക്ഷൻ ഹൗസുമായി ഏകോപിപ്പിച്ചുകൊണ്ട് പാതിരാത്രിയിൽ തന്നെ കാര്യങ്ങൾ സ്വയം പരിഹരിക്കാനുള്ള കഴിവിനുമായാണ് നിങ്ങൾ മുൻഗണന നൽകുന്നത്. AutoMakler നിർമ്മിച്ചപ്പോൾ ഞാൻ നേരിട്ട സാഹചര്യം ഇതാണ്. ലൈവ് ഓക്ഷൻ സ്ക്രാപ്പിംഗ് (scraping), Carfax ലുക്കപ്പുകൾ മുതൽ ഡെലിവറി എസ്റ്റിമേറ്റുകളും പേയ്മെന്റ് പ്രോസസ്സിംഗും വരെ എല്ലാം ഈ പ്ലാറ്റ്ഫോം കൈകാര്യം ചെയ്യുന്നു. ഇത് യഥാർത്ഥ ഉപഭോക്താക്കൾക്കായി പ്രവർത്തിക്കുന്ന ഒരു പ്രൊഡക്ഷൻ സിസ്റ്റമാണ്, കൂടാതെ മിക്ക ഡെവലപ്പർമാരും 'അഗ്രസീവ്ലി ബോറിംഗ് സ്റ്റാക്ക്' (aggressively boring stack) എന്ന് വിളിക്കുന്ന സാങ്കേതികവിദ്യയിലാണ് ഇത് പ്രവർത്തിക്കുന്നത്.
ആരും അവതരിപ്പിക്കാൻ ആഗ്രഹിക്കാത്ത സ്റ്റാക്ക്
ഇവിടെ React ഇല്ല. Vue ഇല്ല. Redis ഇല്ല, Celery ഇല്ല, WebSocket സെർവറും ഇല്ല. ബാക്കെൻഡ് പ്ലെയിൻ Python ഉപയോഗിച്ചുള്ള FastAPI ആണ്. ഡാറ്റാബേസ് PostgreSQL ആണ്. ഫ്രണ്ട്എൻഡ് Jinja2 ടെംപ്ലേറ്റുകൾ, Bootstrap, പിന്നെ അല്പം vanilla JavaScript എന്നിവ ഉപയോഗിച്ച് സെർവർ റെൻഡർ ചെയ്യുന്ന HTML ആണ്. സ്ക്രാപ്പിംഗിനായി ഞാൻ Playwright ഉപയോഗിക്കുന്നു. എല്ലാം നേരിട്ട് HTML നൽകുന്ന ഒരു സിംഗിൾ Python പ്രോസസ്സായി പ്രവർത്തിക്കുന്നു.
ഇവിടെ ബിൽഡ് സ്റ്റെപ്പുകൾ ഇല്ല. പരിശോധിക്കേണ്ട node_modules ഫോൾഡറുകളോ, കോൺഫിഗർ ചെയ്യേണ്ട ട്രാൻസ്പൈലറുകളോ (transpilers), പിന്തുടരേണ്ട ഫ്രണ്ട്എൻഡ് ഫ്രെയിംവർക്ക് മാറ്റങ്ങളോ ഇല്ല. ഞാൻ ഡെപ്ലോയ് ചെയ്യുമ്പോൾ, ബണ്ട്ലറുകളുടെ (bundlers) ഒരു പൈപ്പ്ലൈൻ നിയന്ത്രിക്കുന്നതിന് പകരം Python ഫയലുകളും ടെംപ്ലേറ്റുകളും മാത്രമാണ് മാറ്റുന്നത്. ആ ലാളിത്യമാണ് ഇതിന്റെ ലക്ഷ്യം.
ഒരു മെസേജ് ബ്രോക്കർ ഇല്ലാതെ എങ്ങനെ ജോബ് ക്യൂ (Queue) ചെയ്യാം?
ഒരു ലൈവ് കാർ ഓക്ഷൻ സ്ക്രാപ്പ് ചെയ്യുന്നത് ഒരേസമയം (synchronously) ചെയ്യാൻ കഴിയില്ല. Playwright പേജ് ലോഡ് ചെയ്യാനും JavaScript പ്രവർത്തിപ്പിക്കാനും ഡാറ്റ എടുക്കാനും എടുക്കുന്ന സമയം കാരണം ഒരു സ്ക്രാപ്പിംഗിന് ഏതാനും സെക്കൻഡുകൾ എടുത്തേക്കാം. ഈ സമയത്ത് ഉപഭോക്താവിനെ കാത്തുനിർത്തുന്നത് ശരിയല്ല. Redis ഇൻസ്റ്റാൾ ചെയ്യാനും Celery കോൺഫിഗർ ചെയ്യാനും ഒരു വർക്കർ പൂൾ (worker pool) സജ്ജീകരിക്കാനുമാണ് സാധാരണ നിർദ്ദേശിക്കുന്നത്. എന്നാൽ ഞാൻ ഇവയെല്ലാം ഒഴിവാക്കി.
പകരം, AutoMakler അതിന്റെ ജോബ് ക്യൂവിനായി Postgres ഉപയോഗിക്കുന്നു. ഒരു ഉപഭോക്താവ് സ്ക്രാപ്പിംഗ് ആവശ്യപ്പെടുമ്പോൾ, ആപ്ലിക്കേഷൻ 'pending' സ്റ്റാറ്റസോടെ ഒരു പുതിയ വരി (row) tasks ടേബിളിൽ എഴുതുന്നു. ഒരു asyncio ബാക്ക്ഗ്രൗണ്ട് ടാസ്ക് ആ വരി എടുത്ത് ബ്രൗസർ സ്ക്രാപ്പിംഗ് ആരംഭിക്കുന്നു. ഇതിനിടയിൽ, ബ്രൗസർ സ്റ്റാറ്റസ് പരിശോധിക്കാനായി ഓരോ മൂന്ന് സെക്കൻഡിലും ഒരു ലൈറ്റ് വെയ്റ്റ് എൻഡ്പോയിന്റിലേക്ക് (endpoint) പോൾ (poll) ചെയ്യുന്നു. വരി 'completed' എന്ന് മാറുന്നതോടെ പേജ് റിഫ്രഷ് ചെയ്യപ്പെടുകയും ഫലങ്ങൾ കാണിക്കുകയും ചെയ്യുന്നു.
ഈ രീതി ഫലപ്രദമാകുന്നത് പോളിംഗ് ഇടവേളകൾ (polling interval) കൃത്യമായ അളവിലായതുകൊണ്ടാണ്; അത് വേഗതയുള്ളതായി തോന്നിപ്പിക്കുകയും എന്നാൽ സെർവറിന് അമിതഭാരം ഉണ്ടാക്കാതിരിക്കുകയും ചെയ്യുന്നു. ഒരു കമ്പ്യൂട്ടറിനെ സംബന്ധിച്ചിടത്തോളം മൂന്ന് സെക്കൻഡ് എന്നത് ഒരു വലിയ സമയമാണ്, എന്നാൽ ഒരു പുറത്തുള്ള ഓക്ഷൻ സൈറ്റിനായി കാത്തിരിക്കുന്ന മനുഷ്യന് അത് കാര്യമായി ശ്രദ്ധയിൽപ്പെടില്ല. ഡാറ്റാബേസ് സ്വാഭാവികമായി തന്നെ കൺകറൻസി (concurrency) കൈകാര്യം ചെയ്യുന്നു, ജോബുകൾ വെറും Postgres വരികൾ മാത്രമായതുകൊണ്ട്, Celery ലോഗുകളോ Redis കീകളോ പരിശോധിക്കുന്നതിന് പകരം ഒരു ലളിതമായ SQL ക്വറിയിലൂടെ എനിക്ക് ക്യൂ പരിശോധിക്കാനും സാധിക്കുന്നു.
ഒരു വർക്കർ പൂൾ ഇല്ലാതെ സെർവർ പ്രവർത്തനക്ഷമമായി നിലനിർത്തുന്നത് എങ്ങനെ?
ബ്രൗസർ ഓട്ടോമേഷൻ മെമ്മറി കൂടുതൽ ഉപയോഗിക്കുന്ന ഒന്നാണ്. ഒരേസമയം ഒരുപാട് Playwright ഇൻസ്റ്റൻസുകൾ പ്രവർത്തിപ്പിച്ചാൽ നിങ്ങളുടെ സെർവർ തകർന്നേക്കാം. കൺകറൻസി പരിധികൾ നിശ്ചയിച്ച ഒരു മാനേജ്ഡ് വർക്കർ പൂൾ ഉപയോഗിക്കുക എന്നതാണ് സാധാരണ പരിഹാരം. എന്നാൽ ഞാൻ ഒരു വരി Python ഉപയോഗിക്കുന്നു: asyncio.Semaphore.
ഒരേസമയം എത്ര ബ്രൗസർ ഇൻസ്റ്റൻസുകൾ പ്രവർത്തിപ്പിക്കാം എന്ന് സെമാഫോർ (semaphore) നിയന്ത്രിക്കുന്നു. ഒരു പുതിയ സ്ക്രാപ്പിംഗ് റിക്വസ്റ്റ് വരുമ്പോൾ, അത് ഉടൻ തന്നെ ഒരു സ്ലോട്ട് എടുക്കുകയോ അല്ലെങ്കിൽ ഒരു സ്ലോട്ട് ഒഴിവുണ്ടാകുന്നത് വരെ കാത്തുനിൽക്കുകയോ ചെയ്യുന്നു. ഇതെല്ലാം ഒരേ പ്രോസസ്സിനുള്ളിൽ തന്നെ നടക്കുന്നു. പരാജയപ്പെടാൻ പുറത്തൊരു ഓർക്കസ്ട്രേറ്ററോ (orchestrator), നിശബ്ദമായി നിലയ്ക്കാൻ ഒരു വർക്കർ പ്രോസസ്സോ, നിരീക്ഷിക്കാൻ അധിക ഇൻഫ്രാസ്ട്രക്ചറോ ഇല്ല. എന്റെ മെമ്മറി ഉപയോഗം പ്രവചിക്കാവുന്നതാണ്, കൂടാതെ സെർവറിനെ സംരക്ഷിക്കുന്ന കോഡ് അതിന്റെ തൊട്ടടുത്താണ്, അത് ഒരു ഡെപ്ലോയ്മെന്റ് മാനിഫെസ്റ്റിൽ ഒളിപ്പിച്ചു വെച്ചിരിക്കുന്നതല്ല.
ഒരു കോൾബാക്ക് URL ഉപയോഗിച്ച് പണം കൈമാറുന്നത് എങ്ങനെ?
പേയ്മെന്റ് പ്രോസസ്സിംഗ് എനിക്ക് മാറ്റാൻ കഴിയാത്ത ഒരു തടസ്സം ഉണ്ടാക്കി. എന്റെ പേയ്മെന്റ് ഗേറ്റ്വേയ്ക്ക് ഓരോ മെർച്ചന്റ് അക്കൗണ്ടിനും കൃത്യം ഒരു കോൾബാക്ക് URL മാത്രമേ അനുവദിക്കൂ, എന്നാൽ എനിക്ക് രണ്ട് വ്യത്യസ്ത പ്രോജക്റ്റുകളുടെ ഇടപാടുകൾ ആ ഒറ്റ അക്കൗണ്ട് വഴി പ്രോസസ്സ് ചെയ്യേണ്ടതായിരുന്നു. രണ്ടാമതൊരു മെർച്ചന്റ് പ്രൊഫൈൽ ഉണ്ടാക്കുന്നത് അധിക ഫീസും, കൂടുതൽ നിയമപരമായ നടപടികളും (compliance), ഒരു ചെറിയ ബ്രോക്കറേജ്ക്ക് സമയം ലഭിക്കാത്ത അനാവശ്യ പേപ്പർ വർക്കുകളും ഉണ്ടാക്കുമായിരുന്നു.
ഉപഭോക്താവിനെ ഗേറ്റ്വേയിലേക്ക് അയക്കുന്നതിന് മുമ്പ് ഓർഡർ ഐഡി (order ID) സ്ട്രിംഗിൽ പ്രോജക്റ്റ് പേര് നേരിട്ട് ഉൾപ്പെടുത്തുക എന്നതായിരുന്നു പരിഹാരം. കോൾബാക്ക് എന്റെ സെർവറിൽ എത്തുമ്പോൾ, AutoMakler ആ ഐഡി ഡീകോഡ് ചെയ്യുകയും പേയ്മെന്റ് ഏത് പ്രോജക്റ്റിന്റേതാണെന്ന് തിരിച്ചറിയുകയും ശരിയായ ഇന്റേണൽ ഹാൻഡ്ലറിലേക്ക് നോട്ടിഫിക്കേഷൻ എത്തിക്കുകയും ചെയ്യുന്നു. നിലവിലുള്ള ലോജിക് മാറ്റേണ്ടി വന്നില്ല. ഇത് ഒരു 'അഡിറ്റീവ് ഡിസൈൻ' (additive design) ആണ്: ഞാൻ പേയ്മെന്റ് ഫ്ലോ വീണ്ടും എഴുതിയില്ല, പകരം ഐഡിക്ക് കൂടുതൽ വിവരങ്ങൾ നൽകാൻ സഹായിച്ചു. ഇത് പിന്നീട് ചിന്തിക്കുമ്പോൾ വളരെ ലളിതമായി തോന്നുമെങ്കിലും, വലിയ രീതിയിലുള്ള ആർക്കിടെക്ചറൽ മാറ്റങ്ങൾ ഒഴിവാക്കി മണിക്കൂറുകൾ ലാഭിക്കാൻ സഹായിക്കുന്ന ഒരു രീതിയാണ്.
WebSockets ഇല്ലാതെ പ്രവർത്തിക്കുന്ന ചാറ്റ്
Customer support chat is usually where engineers cave and add WebSockets. I needed in-app messaging, but I also needed to keep the infrastructure footprint tiny. So I reused the same polling strategy that powers the auction scrapes.
Messages are stored in Postgres. When a user sends a message, it writes to the table. The client polls for updates, and the UI reflects new messages and read receipts in near real-time. To keep this fast even as the conversation table grows, I added a Postgres partial index that only covers unread messages for active conversations. The database does not waste cycles scanning old history, and the query planner can satisfy most chat lookups with a tight index range scan.
For a support chat where a few seconds of latency is acceptable, this is perfectly adequate. The users get the feedback they need, and I never had to debug a stale WebSocket connection or manage a separate socket server.
The Honest Downsides
This architecture makes real trade-offs, and pretending otherwise would be dishonest. Polling is chatty. Every three seconds, every active client hits the server. The bandwidth and query load are higher than a persistent socket connection would demand. If the Python process restarts, any in-flight background task dies immediately because there is no external worker to pick it back up. I accept this because the tasks are small and the cost of a retry is low. A failed browser scrape can simply be re-triggered by the user.
There is also a ceiling to this approach. If AutoMakler ever needs to serve thousands of simultaneous scrapes, the single-process model with polling will strain. But that is not the business I am in. I need reliability for dozens of concurrent users, not
