الانتقال إلى المحتوى

من الفريق

تشغيل Laravel queues في الإنتاج دون فقدان المهام

الـ queues تعمل على الحاسوب المحمول. أما في الإنتاج فيعمل الـ workers لأيام، وتفشل المهام في منتصفها، ويحدث النشر وسط دفعة. هذا ما ينبغي تقريره عمداً.

قراءة 5 دقيقة

تعمل Laravel queues في الجزء من التطبيق الذي ينفَّذ بعد إرسال استجابة HTTP: إرسال البريد، وتوليد ملف PDF، ومزامنة الطلب مع نظام المحاسبة. هذه المقالة موجّهة للمطوّرين والقادة التقنيين الذين تعمل لديهم الـ queues محلياً ويحتاجون الآن إلى أن تتصرف بشكل صحيح في الإنتاج، حيث يعمل الـ workers لأيام، وتفشل المهام في منتصفها، ويحدث النشر في منتصف دفعة. لا شيء من هذا غريب؛ ومعظمه يتلخص في اتخاذ بضعة قرارات عمداً بدل قبول الإعدادات الافتراضية.

ما الذي ينبغي أن يوضع في job مُجدوَل؟

الاختبار بسيط: إن كان المستخدم لا يحتاج إلى النتيجة لعرض الشاشة التالية، فمكانها job. رسائل البريد، والإشعارات، والـ webhooks إلى أطراف ثالثة، ومعالجة الصور، وتوليد التقارير، وأي شيء يتحدث إلى شبكة لا تتحكم بها. ينبغي أن يكتب الطلب في قاعدة البيانات، ويرسل الـ job، ويعود.

قاعدتان تبقيان الـ jobs سليمة. أولاً، مرّر المعرّفات لا الكائنات. ستقوم SerializesModels بسرور بتسلسل نموذج كامل، لكن الـ job عندها يعمل على حالة ذلك النموذج وقت الإرسال في ذهنك، ووقت التنفيذ في الواقع. أرسل بمعرّف وحمّل بيانات حديثة داخل handle. ثانياً، اجعل كل job يفعل شيئاً واحداً. فالـ job الذي يرسل الفاتورة ويحدّث دفتر الأستاذ ويُخطر مدير الحساب سيفشل عند الثلث ويعيد المحاولة للثلاثة كلها. قسّمه، أو اربطه في سلسلة عبر Bus::chain.

Redis أم database driver لـ Laravel queues؟

مشغّل database مناسب للبداية وللأدوات الداخلية منخفضة الحجم. لا يحتاج إلى بنية تحتية إضافية، والـ job الفاشل صف يمكنك فحصه بعميل SQL. ضعفه في التزاحم: كل worker يستطلع الجدول نفسه، وتحت الحمل ترى انتظارات على الأقفال وجدول jobs ينمو أسرع مما يُنظَّف.

لأي شيء يواجه العملاء، استخدم redis. فهو أسرع، ويتعامل مع workers كثيرة دون تزاحم على الأقفال، وهو المشغّل الذي بُني Laravel Horizon من أجله. وإن كنت تشغّل Redis أصلاً للتخزين المؤقت أو الجلسات، فكلفة إضافة اتصال queue تقارب الصفر. أبقِ الـ queue على قاعدة Redis أو نسخة منفصلة عن التخزين المؤقت، حتى لا يمسّ أمر cache:clear أو سياسة إخلاء مضبوطة للتخزين المؤقت المهامَ المعلّقة أبداً.

يستحق Horizon مكانه في اليوم الذي يصبح لديك فيه أكثر من queue واحد. يمنحك لوحة للإنتاجية وأزمنة الانتظار والإخفاقات لكل queue، وإعداد supervisor في الكود بدل ملف systemd، وأمر horizon:terminate لإعادة تشغيل رشيقة. عرّف الـ queues حسب الأولوية لا حسب الميزة: high لأي شيء ينتظره مستخدم، وdefault للباقي، وlow للتقارير والتصدير. وامنح high مجموعة workers خاصة به حتى لا يؤخّر تصدير بطيء إعادة تعيين كلمة مرور أبداً.

كيف تعمل إعادة المحاولة والـ backoff والـ idempotency معاً؟

اضبط $tries و$backoff على كل job صراحةً. قد يستخدم webhook إلى API خارجي ثلاث محاولات مع backoff بقيم [10, 60, 300] ثانية؛ أما job يقرأ من قاعدة بياناتك فيريد على الأرجح محاولة واحدة، لأنه إن فشل مرة فسيفشل ثانية، وإعادة المحاولة تخفي الخطأ فقط.

لا تفيد إعادة المحاولة إلا إن أمكن تشغيل الـ job مرتين بأمان. هذا هو idempotency، وهو الجزء الذي تتخطاه الفرق. قبل إرسال بريد، تحقق مما إذا كان قد أُرسل. قبل خصم بطاقة، استخدم مفتاح idempotency لدى مزوّد الدفع. قبل الإدراج، استخدم updateOrCreate أو قيداً فريداً والتقط المخالفة. اكتب كل job كأنه سيُشغَّل مجدداً بعد انهيار عند أي سطر، لأن ذلك سيحدث.

استخدم ShouldBeUnique للـ jobs التي لا ينبغي وضعها في الـ queue مرتين بالتزامن للكيان نفسه، ووسيط WithoutOverlapping للـ jobs التي يجوز وضعها في الـ queue لكن يجب ألا تعمل بالتوازي. واضبط timeout أقل من قيمة retry_after لدى الـ worker، وإلا سُلّم job بطيء إلى worker ثانٍ بينما الأول ما زال ينفّذه، وهذه أكثر الطرق شيوعاً للحصول على آثار جانبية مكررة.

كيف تراقب المهام الفاشلة وتعيد تشغيل الـ workers عند النشر؟

الـ job الفاشل ليس رسالة خطأ؛ بل صف في failed_jobs ينتظر أن يلاحظه أحد. اربط Queue::failing أو حدث JobFailed بما ينظر إليه الفريق أصلاً، ونبّه على حجم جدول الفشل لا على كل فشل منفرد. أعد المحاولة بـ queue:retry بعد إصلاح السبب لا قبله، ونظّف الجدول بجدولة عبر queue:prune-failed.

راقب عمر أقدم job معلّق لكل queue وعدد الـ workers الحية فعلاً؛ يعرض Horizon كليهما، ومن دونه يستطيع queue:monitor التنبيه على حجم الـ queue.

يحمّل الـ workers الكود مرة واحدة ويبقونه في الذاكرة. وبعد النشر، يظل كل worker يعمل على الإصدار السابق حتى يُطلب منه التوقف. اجعل queue:restart (أو horizon:terminate) الخطوة الأخيرة في كل سكربت نشر، وشغّل الـ workers تحت مدير عمليات مثل Supervisor أو systemd ليعودوا إلى العمل. وإعادة التشغيل رشيقة: ينهي كل worker مهمته الحالية أولاً.

أخيراً، عامل إعداد الـ queue كجزء من التسليم لا كعمل مؤجَّل لما بعد الإطلاق. في مشاريعنا يُدرج ضمن النطاق ثابت السعر من المرحلة الأولى، لأن الـ queue الذي يُعدّ على عجل بعد الإطلاق هو حيث يحدث فقدان البيانات الصامت.