← كل المقالات

حل مشكلة توقف Laravel Queue وضبط Supervisor

شخّص توقف طابور Laravel بفحص الاتصال واسم الطابور وتشغيل العامل يدوياً ثم مطابقة إعداد Supervisor والصلاحيات والمهلات وخطوات النشر.

يبني بكري عبدالسلام مواقع الويب والتطبيقات والتكاملات ومنتجات ووردبريس، ويوثق مركز بكري التقني القرارات التقنية وراء هذا العمل.

الخلاصة

تتبع Job واحدة من Dispatch حتى التنفيذ: أثبت أولاً أن Worker يعمل يدوياً بنفس مستخدم الإنتاج، ثم اجعل Supervisor يشغل الأمر نفسه ويحافظ عليه.

عبارة «Laravel Queue لا تعمل» قد تصف أكثر من عطل: مهمة لم تدخل الطابور، أو ذهبت إلى اتصال مختلف، أو تنتظر في اسم طابور لا يقرأه أي عامل، أو بدأت ثم فشلت. يحافظ Supervisor على استمرار العملية، لكنه لا يصلح إعداد Laravel الخاطئ.

اختر Job اختبار واحدة بمعرف واضح وتتبعها خطوة بخطوة.

تأكد أن اتصال الطابور ليس sync

راجع إعداد الإنتاج:

QUEUE_CONNECTION=redis

إذا كانت القيمة sync فستعمل Job داخل Web Request ولن تظهر داخل Worker. ومع Config Cache لا يكفي تعديل .env:

php artisan config:show queue
php artisan config:clear
php artisan config:cache

نفذ ذلك ضمن Deploy منظم، وتأكد أن PHP-FPM وCLI يقرآن نفس Release ونفس Environment.

يجب أن تنفذ Job واجهة ShouldQueue. وإذا تم Dispatch داخل Database Transaction، استخدم الإرسال بعد Commit عندما تعتمد Job على بيانات لم تثبت بعد.

طابق الاتصال واسم الطابور

الـConnection تحدد Backend مثل Redis أو Database، بينما Queue هي مسار مثل emails أو default.

SendWelcomeEmail::dispatch($user)
    ->onConnection('redis')
    ->onQueue('emails');

لن تلتقطها عملية تقرأ default فقط. شغّل Worker مطابقاً:

php artisan queue:work redis --queue=emails,default -vvv

للتشخيص شغّل عملية واحدة أمامك وبنفس مستخدم الإنتاج:

cd /var/www/example/current
sudo -u www-data php artisan queue:work redis \
  --queue=emails,default \
  --sleep=3 \
  --tries=3 \
  --timeout=90 \
  --once -vvv

إذا فشل هذا الأمر أصلح Laravel أو الاتصال أو الصلاحيات أولاً. نجاحه يعني أن النقطة التالية هي Supervisor.

افحص المهام المنتظرة والفاشلة

استخدم ما يناسب Driver: جدول jobs، طول Redis Queue، SQS Metrics، أو Horizon. وللأعمال الفاشلة:

php artisan queue:failed
php artisan queue:retry all

لا تعِد كل Jobs قبل إصلاح الاستثناء؛ قد تكرر Email أو Payment أو تضغط على API خارجية. اقرأ storage/logs/laravel.log وسجل Worker عند وقت الفشل.

انتبه إلى Delays وRate Limiting وUnique Jobs وMiddleware التي تعيد Job إلى Queue. فراغ Queue لا يعني نجاح العمل دائماً؛ قد يستهلكها Worker ثم يفشل بسرعة.

أنشئ إعداد Supervisor من أمر نجح يدوياً

مثال عملي:

[program:example-worker]
process_name=%(program_name)s_%(process_num)02d
directory=/var/www/example/current
command=/usr/bin/php artisan queue:work redis --queue=emails,default --sleep=3 --tries=3 --timeout=90 --max-time=3600
user=www-data
numprocs=2
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=120
redirect_stderr=true
stdout_logfile=/var/log/supervisor/example-worker.log

استخدم مسارات كاملة ونفس المستخدم الذي نجح في الاختبار. اجعل stopwaitsecs أطول من أطول Job شرعية حتى لا يقتل Supervisor العمل أثناء الإيقاف.

حمّل الإعداد:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status "example-worker:*"

ظهور BACKOFF أو FATAL يعني أن العملية تنتهي مباشرة. شغّل command نفسه كمستخدم user واقرأ stdout_logfile. الأسباب الشائعة: PHP Path خاطئ، Working Directory غير صحيح، ملفات Release غير مقروءة، أو storage غير قابل للكتابة.

نسق مهلة العامل مع retry_after

يجب أن تكون --timeout أقصر بعدة ثوانٍ من retry_after في Connection:

// config/queue.php
'retry_after' => 120,
command=/usr/bin/php artisan queue:work redis --timeout=90 --tries=3

إذا أصبح Job متاحاً لإعادة المحاولة قبل انتهاء Worker الأول فقد ينفذ مرتين. ضع أيضاً Timeouts مستقلة لاتصالات HTTP وقاعدة البيانات.

صمم Side Effects لتتحمل Retry. الـWebhooks والمدفوعات تحتاج Idempotency Key أو Unique Constraint؛ راجع منع تكرار Webhooks في Laravel.

أعد تشغيل العمال بعد كل نشر

Workers عمليات طويلة العمر ولا تقرأ الكود الجديد تلقائياً:

php artisan queue:restart

يحفظ Laravel إشارة Restart داخل Cache، لذلك يجب أن يستخدم Workers نفس الـCache المتوقع. ينهي Worker الـJob الحالية ثم يخرج، ويقوم Supervisor ببدء عملية جديدة.

عند استخدام Symlink باسم current تأكد أن العمليات الجديدة تبدأ من الإصدار الجديد. يشرح نشر Laravel بدون توقف ترتيب الإصدار والـMigrations والـWorkers.

تحقق وراقب النتيجة

سجل حقولاً آمنة: Job ID، Class، Queue، رقم المحاولة، وقت البداية والنهاية، والنتيجة. لا تسجل Payload كاملة أو أسراراً.

تأكد أن Job ظهرت في Backend الصحيح، التقطها Worker، نفذت أثرها مرة واحدة، بقي Supervisor في RUNNING، وعمل Restart بعد النشر. راقب Queue Latency وعدد Failed Jobs وليس حالة Process فقط.

أخطاء شائعة

الأسباب المتكررة: sync في الإنتاج، اسم Queue غير متطابق، Config Cache قديم، مستخدم Supervisor خاطئ، Worker يشير إلى Release قديم، وtimeout أكبر من retry_after.

لا تضف Workers أكثر قبل إثبات أن Worker واحدة صحيحة. حدد Concurrency حسب زمن Job والذاكرة وحدود الخدمات الخارجية.

أسئلة شائعة

لماذا تنتظر المهام رغم أن Supervisor يعرض RUNNING؟

الحالة تثبت وجود Process فقط. قد تقرأ Connection أو Queue مختلفة، أو تستخدم Config قديمة، أو تفشل Jobs سريعاً. قارن أمر Supervisor بوجهة Job الفعلية.

هل أستخدم queue:listen أم queue:work في الإنتاج؟

استخدم queue:work عادة كعملية إنتاج فعالة يديرها Supervisor وتُعاد أثناء Deploy. أما queue:listen فيعيد تحميل التطبيق لكل Job وله تكلفة مختلفة.

مراجع رسمية

لديك سؤال عن هذا الدليل أو فكرة لتعاون تقني؟ تواصل مع بكري عبر المركز التقني.

نهاية الملاحظة.