عندما تدير شركة وساطة سيارات في أذربيجان وتستورد سيارات تالفة من الولايات المتحدة، فإن مشكلات البرمجيات لديك تختلف عن تلك التي تواجهها الشركات الناشئة في وادي السيليكون. أنت لا تسعى لتحسين الأداء لخدمة مليون مستخدم متزامن، بل تسعى لتحقيق الوضوح، واستمرارية الخدمة (uptime)، والقدرة على إصلاح الأمور بنفسك في منتصف الليل بينما تنسق مع دار مزادات تبعد عنك اثنتي عشرة منطقة زمنية. هذا هو بالضبط الموقف الذي وجدت نفسي فيه عندما بنيت AutoMakler. تتعامل المنصة مع كل شيء، بدءاً من كشط بيانات المزادات المباشرة (live auction scraping) والبحث في Carfax، وصولاً إلى تقديرات التسليم ومعالجة المدفوعات. إنه نظام إنتاجي حقيقي يخدم عملاء حقيقيين، ويعمل بما قد يسميه معظم المطورين "مجموعة تقنيات (stack) مملة للغاية".

مجموعة التقنيات التي لا أحد يرغب في الترويج لها

لا يوجد React. لا يوجد Vue. لا يوجد Redis، ولا Celery، ولا خادم WebSocket. الواجهة الخلفية (backend) هي FastAPI باستخدام Python العادية. قاعدة البيانات هي PostgreSQL. الواجهة الأمامية (frontend) هي HTML يتم رندرتها على الخادم (server-rendered) باستخدام قوالب Jinja2، وBootstrap، ولمسة من vanilla JavaScript. ولعملية الكشط (scraping)، أستخدم Playwright. يعمل كل شيء كعملية Python واحدة تقدم HTML مباشرة.

لا توجد خطوة بناء (build step). لا توجد مجلدات node_modules لفحصها، ولا مترجمات (transpilers) لتهيئتها، ولا تقلبات في أطر عمل الواجهة الأمامية لمواكبتها. عندما أقوم بالنشر (deploy)، فإنني أنقل ملفات Python والقوالب، ولا أقوم بتنسيق سلسلة من أدوات التجميع (bundlers). هذه البساطة ليست تنازلاً، بل هي الهدف الأساسي.

كيفية جدولة المهام بدون وسيط رسائل

لا يمكن إجراء عملية كشط بيانات لمزاد سيارات مباشر بشكل متزامن (synchronously). قد تستغرق عملية كشط واحدة عدة ثوانٍ حيث يقوم Playwright بتحميل الصفحة، وتنفيذ JavaScript، واستخراج البيانات. حظر المستخدم أثناء حدوث ذلك ليس خياراً مطروحاً. تقول القواعد القياسية: قم بتثبيت Redis، وتهيئ Celery، وقم بتشغيل مجموعة من العمال (worker pool). لقد تخطيت كل ذلك.

بدلاً من ذلك، يستخدم AutoMakler قاعدة بيانات Postgres كطابور مهام خاص به. عندما يبدأ المستخدم عملية كشط، يقوم التطبيق بكتابة صف جديد في جدول المهام (tasks table) بحالة "قيد الانتظار" (pending). تقوم مهمة خلفية باستخدام asyncio بالتقاط ذلك الصف وبدء عملية كشط المتصفح. في هذه الأثناء، يقوم المتصفح بعملية استطلاع (polling) لنقطة نهاية (endpoint) خفيفة كل ثلاث ثوانٍ للتحقق من الحالة. عندما يتم تحديث الصف إلى "مكتمل" (completed)، يتم تحديث الصفحة وعرض النتائج.

يعمل هذا النمط لأن فاصل الاستطلاع قصير بما يكفي ليشعر المستخدم بالاستجابة، ولكنه طويل بما يكفي لتجنب إرهاق الخادم. ثلاث ثوانٍ هي دهر بالنسبة للكمبيوتر، وبالكاد تُلاحظ من قبل إنسان ينتظر موقع مزادات خارجي. تتعامل قاعدة البيانات مع التزامن (concurrency) بشكل أصلي، ولأن المهام هي مجرد صفوف في Postgres، يمكنني فحص الطابور باستخدام استعلام SQL بسيط بدلاً من البحث في سجلات Celery أو مفاتيح Redis.

الحفاظ على استمرارية الخادم بدون مجموعة عمال

أتمتة المتصفح تستهلك الكثير من الذاكرة. إذا قمت بتشغيل الكثير من مثيلات Playwright في وقت واحد، فسوف ينهار خادمك. الحل التقليدي هو استخدام مجموعة عمال مدارة مع حدود للتزامن، وغالباً ما تكون مدعومة بتوليفة Redis وCelery نفسها. أنا أستخدم سطراً واحداً من Python: وهو asyncio.Semaphore.

يحدد الـ semaphore عدد مثيلات المتصفح (browser instances) التي يمكن تشغيلها في وقت واحد. عندما يأتي طلب كشط جديد، فإنه إما يحجز مكاناً فوراً أو ينتظر حتى يفرغ مكان. يحدث كل هذا داخل العملية نفسها. لا يوجد منسق خارجي قد يفشل، ولا عملية عامل قد تموت بصمت، ولا بنية تحتية إضافية لمراقبتها. تظل ذاكرتي قابلة للتنبؤ، والكود الذي يحمي الخادم موجود بجوار الكود الذي يستخدمه مباشرة، وليس مخفياً في ملف إعدادات النشر (deployment manifest).

توجيه الأموال باستخدام رابط استدعاء واحد

فرضت معالجة المدفوعات قيداً لم أتمكن من تغييره. تسمح بوابة الدفع الخاصة بي برابط استدعاء (callback URL) واحد فقط لكل حساب تاجر، لكنني كنت بحاجة إلى معالجة المعاملات لمشروعين منفصلين من خلال ذلك الحساب الواحد. بناء ملف تعريف تاجر ثانٍ كان سيعني رسوماً إضافية، وامتثالاً إضافياً، وأوراقاً إضافية لا تملك شركة وساطة صغيرة الوقت للتعامل معها.

كان الحل هو تشفير اسم المشروع مباشرة في سلسلة معرف الطلب (order ID string) قبل إرسال العميل إلى بوابة الدفع. عندما يصل رابط الاستدعاء إلى خادمي، يقوم 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