جب آپ آذربائیجان میں کار بروکرج چلا رہے ہوں اور امریکہ سے حادثہ شدہ (salvage) گاڑیاں درآمد کر رہے ہوں، تو آپ کے سافٹ ویئر کے مسائل سلیکون ویلی کے کسی اسٹارٹ اپ سے مختلف ہوتے ہیں۔ آپ دس لاکھ بیک وقت استعمال کرنے والے صارفین (concurrent users) کے لیے سسٹم کو بہتر نہیں بنا رہے ہوتے۔ آپ کا مقصد وضاحت، اپ ٹائم، اور آدھی رات کو خود چیزوں کو ٹھیک کرنے کی صلاحیت ہے جبکہ آپ بارہ ٹائم زون دور کسی آکشن ہاؤس کے ساتھ رابطہ کر رہے ہوں۔ 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 فولڈرز نہیں ہیں، کنفیگر کرنے کے لیے کوئی ٹرانسپائلرز نہیں ہیں، اور ساتھ چلنے کے لیے کوئی فرنٹ اینڈ فریم ورک کی تبدیلیوں کا پیچھا نہیں کرنا پڑتا۔ جب میں اسے ڈیپلائے کرتا ہوں، تو میں Python فائلیں اور ٹیمپلیٹس منتقل کرتا ہوں، نہ کہ بنڈلرز (bundlers) کی کوئی پائپ لائن ترتیب دیتا ہوں۔ یہ سادگی کوئی سمجھوتہ نہیں ہے۔ یہی تو اصل مقصد ہے۔

میسج بروکر کے بغیر جابز کو کیسے کیو (Queue) کریں

لائیو کار آکشن کی اسکریپنگ سنکرون情况下 (synchronously) نہیں ہو سکتی۔ ایک اسکریپنگ میں کئی سیکنڈ لگ سکتے ہیں کیونکہ Playwright پیج لوڈ کرتا ہے، JavaScript چلاتا ہے، اور ڈیٹا نکالتا ہے۔ اس دوران صارف کو روکنا کوئی آپشن نہیں ہے۔ اسٹینڈرڈ طریقہ کار کہتا ہے کہ Redis انسٹال کریں، Celery کنفیگر کریں، اور ورکر پول (worker pool) شروع کریں۔ میں نے یہ سب چھوڑ دیا۔

اس کے بجائے، AutoMakler Postgres کو اپنے جاب کیو (job queue) کے طور پر استعمال کرتا ہے۔ جب کوئی صارف اسکریپنگ شروع کرتا ہے، تو ایپلی کیشن 'tasks' ٹیبل میں 'pending' اسٹیٹس کے ساتھ ایک نئی رو (row) لکھ دیتی ہے۔ ایک asyncio بیک گراؤنڈ ٹاسک اس رو کو اٹھاتا ہے اور براؤزر اسکریپنگ شروع کر دیتا ہے۔ اس دوران، براؤزر اسٹیٹس چیک کرنے کے لیے ہر تین سیکنڈ بعد ایک ہلکے پھلکے اینڈ پوائنٹ (endpoint) کو پول (poll) کرتا ہے۔ جب رو 'completed' میں اپ ڈیٹ ہو جاتی ہے، تو پیج ریفریش ہوتا ہے اور نتائج دکھاتا ہے۔

یہ پیٹرن اس لیے کام کرتا ہے کیونکہ پولنگ کا وقفہ اتنا کم ہے کہ رسپانسو محسوس ہو، لیکن اتنا لمبا ہے کہ سرور پر بوجھ نہ پڑے۔ کمپیوٹر کے لیے تین سیکنڈ ایک طویل عرصہ ہے لیکن کسی انسان کے لیے جو بیرونی آکشن سائٹ کا انتظار کر رہا ہو، یہ بمشکل محسوس ہوتا ہے۔ ڈیٹا بیس قدرتی طور پر کنکرنسی (concurrency) کو سنبھالتا ہے، اور چونکہ جابز محض Postgres میں رو (rows) ہیں، اس لیے میں Celery لاگز یا Redis کیز کو کھنگالنے کے بجائے ایک سادہ SQL کوئری کے ذریعے کیو کا معائنہ کر سکتا ہوں۔

ورکر پول کے بغیر سرور کو زندہ رکھنا

براؤزر آٹومیشن میموری کا بہت زیادہ استعمال کرتی ہے۔ اگر آپ ایک ساتھ بہت زیادہ Playwright انسٹنسز چلا دیں گے تو آپ کا سرور بیٹھ جائے گا۔ روایتی حل کنکرنسی کی حدود کے ساتھ ایک مینیجڈ ورکر پول ہے، جو اکثر اسی Redis اور Celery کے امتزاج پر مبنی ہوتا ہے۔ میں Python کی صرف ایک لائن استعمال کرتا ہوں: ایک asyncio.Semaphore۔

سیمافور (semaphore) اس بات پر حد مقرر کرتا ہے کہ بیک وقت کتنے براؤزر انسٹنس چل سکتے ہیں۔ جب کوئی نیا اسکریپنگ ریکویسٹ آتی ہے، تو یہ یا تو فوری طور پر ایک سلاٹ (slot) حاصل کر لیتا ہے یا ایک سلاٹ خالی ہونے تک انتظار کرتا ہے۔ یہ سب کچھ ایک ہی پروسیس کے اندر ہوتا ہے۔ یہاں ناکام ہونے کے لیے کوئی بیرونی آرکیسٹریٹر (orchestrator) نہیں ہے، کوئی ورکر پروسیس نہیں ہے جو خاموشی سے بند ہو جائے، اور مانیٹر کرنے کے لیے کوئی اضافی انفراسٹرکچر نہیں ہے۔ میری میموری قابلِ پیش گوئی رہتی ہے، اور وہ کوڈ جو سرور کی حفاظت کرتا ہے، وہ اسی کوڈ کے بالکل ساتھ ہوتا ہے جو اسے استعمال کرتا ہے، نہ کہ کسی ڈیپلائمنٹ مینی فیسٹ (deployment manifest) میں چھپا ہوا۔

ایک کال بیک URL کے ذریعے رقم کی روٹنگ کرنا

پیمنٹ پروسیسنگ نے ایک ایسی پابندی لگائی جسے میں تبدیل نہیں کر سکتا تھا۔ میرا پیمنٹ گیٹ وے ہر مرچنٹ اکاؤنٹ کے لیے صرف ایک کال بیک URL کی اجازت دیتا ہے، لیکن مجھے اس ایک اکاؤنٹ کے ذریعے دو الگ الگ پروجیکٹس کے لیے ٹرانزیکشنز پروسیس کرنے کی ضرورت تھی۔ دوسرا مرچنٹ پروفائل بنانے کا مطلب ہوتا اضافی فیس، اضافی تعمیل (compliance)، اور اضافی کاغذی کارروائی جس کے لیے ایک چھوٹی بروکرج کے پاس وقت نہیں ہوتا۔

اس کا حل یہ تھا کہ کسٹمر کو گیٹ وے پر بھیجنے سے پہلے آرڈر آئی ڈی (order ID) اسٹرنگ میں براہ راست پروجیکٹ کا نام شامل (encode) کر دیا جائے۔ جب کال بیک میرے سرور پر پہنچتا ہے، تو AutoMakler اس آئی ڈی کو ڈی کوڈ (decode) کرتا ہے، شناخت کرتا ہے کہ ادائیگی کس پروجیکٹ سے متعلق ہے، اور نوٹیفکیشن کو درست اندرونی ہینڈلر (handler) کی طرف بھیج دیتا ہے۔ موجودہ لاجک کو ہاتھ لگانے کی ضرورت نہیں پڑی۔ یہ "ایڈیٹیو ڈیزائن" (additive design) ہے: میں نے پیمنٹ فلو کو دوبارہ نہیں لکھا، میں نے صرف آئی ڈینٹیفائر میں تھوڑا سا مزید سیاق و سباق (context) شامل کر دیا۔ یہ اس طرح کی ہیکنگ ہے جو بعد میں دیکھنے میں بالکل واضح لگتی ہے لیکن آرکیٹیکچرل مشکل کاموں کے گھنٹوں بچا لیتی ہے۔

چیٹ جو WebSockets کے بغیر کام کرتی ہے

کسٹمر سپورٹ چیٹ عام طور پر وہ جگہ ہوتی ہے جہاں انجینئرز ہار مان لیتے ہیں اور WebSockets کا استعمال شروع کر دیتے ہیں۔ مجھے ان-ایپ میسجنگ کی ضرورت تھی، لیکن میں انفراسٹرکچر کے اثر (footprint) کو بھی بہت کم رکھنا چاہتا تھا۔ اس لیے میں نے اسی پولنگ اسٹریٹیجی کو دوبارہ استعمال کیا جو نیلامی کے اسکریپس (auction scrapes) کو چلاتی ہے۔

پیغامات Postgres میں محفوظ کیے جاتے ہیں۔ جب کوئی صارف پیغام بھیجتا ہے، تو وہ ٹیبل میں لکھا جاتا ہے۔ کلائنٹ اپ ڈیٹس کے لیے پولنگ کرتا ہے، اور UI تقریباً ریئل ٹائم میں نئے پیغامات اور ریڈ رسیدز (read receipts) دکھاتا ہے۔ گفتگو کے ٹیبل کے بڑھنے کے باوجود اسے تیز رکھنے کے لیے، میں نے ایک Postgres partial index شامل کیا جو صرف فعال گفتگو کے غیر پڑھے ہوئے پیغامات کا احاطہ کرتا ہے۔ ڈیٹا بیس پرانی ہسٹری کو اسکین کرنے میں وقت ضائع نہیں کرتا، اور کوئری پلانر (query planner) ایک محدود انڈیکس رینج اسکین کے ذریعے چیٹ کے زیادہ تر لُک اپس (lookups) کو پورا کر سکتا ہے۔

سپورٹ چیٹ کے لیے جہاں چند سیکنڈ کی تاخیر (latency) قابلِ قبول ہو، یہ طریقہ بالکل مناسب ہے۔ صارفین کو وہ فیڈ بیک مل جاتا ہے جس کی انہیں ضرورت ہوتی ہے، اور مجھے کبھی بھی کسی پرانی (stale) WebSocket کنکشن کو ڈی بگ کرنے یا الگ سے ساکٹ سرور کو مینیج کرنے کی ضرورت نہیں پڑی۔

ایماندارانہ خامیاں

یہ آرکیٹیکچر حقیقی سمجھوتوں (trade-offs) پر مبنی ہے، اور اس کے برعکس ظاہر کرنا بے ایمانی ہوگی۔ پولنگ میں بہت زیادہ نیٹ ورک ریکویسٹس ہوتی ہیں۔ ہر تین سیکنڈ میں، ہر فعال کلائنٹ سرور کو ریکویسٹ بھیجتا ہے۔ بینڈوتھ اور کوئری کا بوجھ ایک مستقل ساکٹ کنکشن کے تقاضوں سے زیادہ ہوتا ہے۔ اگر Python پراسیس دوبارہ شروع (restart) ہوتا ہے، تو کوئی بھی جاری بیک گراؤنڈ ٹاسک فوری طور پر ختم ہو جاتا ہے کیونکہ اسے دوبارہ اٹھانے کے لیے کوئی بیرونی ورکر موجود نہیں ہوتا۔ میں اسے اس لیے قبول کرتا ہوں کیونکہ ٹاسک چھوٹے ہیں اور دوبارہ کوشش (retry) کی لاگت کم ہے۔ ایک ناکام براؤزر اسکریپ کو صارف کے ذریعے آسانی سے دوبارہ شروع کیا جا سکتا ہے۔

اس طریقے کی ایک حد (ceiling) بھی ہے۔ اگر AutoMakler کو کبھی ہزاروں بیک وقت ہونے والے اسکریپس (simultaneous scrapes) کو سنبھالنے کی ضرورت پڑی، تو پولنگ والا سنگل پراسیس ماڈل دباؤ کا شکار ہو جائے گا۔ لیکن میرا کاروبار ایسا نہیں ہے۔ مجھے درجنوں بیک وقت استعمال کرنے والے صارفین (concurrent users) کے لیے بھروسہ مندی چاہیے، نہ کہ