عندما تنتقل المهام الثقيلة مثل إرسال البريد، معالجة الصور، توليد التقارير ومزامنة البيانات إلى الخلفية، تصبح الطوابير جزءًا حرجًا من بنية التطبيق. لكن تشغيل Queue Workers وحده لا يكفي؛ تحتاج إلى معرفة معدل المعالجة، وزمن التنفيذ، والمهام الفاشلة، وكيفية توزيع العمال على الطوابير. يقدم Laravel Horizon لوحة رسمية وإعدادًا برمجيًا موحدًا لإدارة طوابير Laravel المدعومة بواسطة Redis.

Horizon ليس نظام Queue مستقلًا؛ بل طبقة إدارة ومراقبة فوق Laravel Redis Queues، تنظم Workers وSupervisors وتعرض مؤشرات التشغيل والفشل في لوحة واحدة.

يعتمد هذا المقال على توثيق Laravel Horizon الرسمي لإصدار Laravel 13.x. راجع نسخة التوثيق المطابقة لإصدار مشروعك، خصوصًا قبل ترقية إصدار رئيسي من Horizon.

ما هو Laravel Horizon؟

Laravel Horizon حزمة رسمية توفر لوحة لمراقبة Redis Queues وإعدادًا قائمًا على الكود لعمال الطوابير. تعرض اللوحة معلومات مثل Job throughput وRuntime والمهام الحديثة والفاشلة، وتتيح البحث بالوسوم. أما ملف config/horizon.php فيجمع إعدادات العمال لكل بيئة، ما يجعلها قابلة للمراجعة والتتبع في Git.

يساعد Horizon على الإجابة عن أسئلة تشغيلية مهمة:

  • هل معدل وصول Jobs أعلى من معدل معالجتها؟
  • أي الطوابير تحتاج إلى Workers إضافية؟
  • ما المهام الأكثر بطئًا أو فشلًا؟
  • كيف نخصص موارد منفصلة لمهام الصور والتقارير والإشعارات؟
  • كيف نعيد تشغيل العمال بعد النشر من دون قطع Job قيد التنفيذ؟

Horizon مقابل Queue Worker التقليدي

الجانبqueue:workHorizon
الـBackendيدعم اتصالات Queue المختلفة.يتطلب Redis Queue.
الإدارةخيارات CLI ومدير عمليات.Supervisors وإعداد موحد حسب البيئة.
الموازنةتوزيع يدوي غالبًا.auto أو simple أو أولوية مرتبة.
الواجهةلا توجد لوحة افتراضية.لوحة للمهام والمقاييس والفشل.
الوسومليست تجربة مراقبة مدمجة.وسوم تلقائية ومخصصة للبحث.

المتطلبات والقيود

  • يجب أن يكون Queue connection المستخدم بواسطة Horizon من نوع Redis.
  • توضح وثائق Laravel 13.x أن Horizon غير متوافق حاليًا مع Redis Cluster.
  • يجب فهم أساسيات Jobs وQueues والمحاولات والفشل قبل ضبط Horizon.
  • تحتاج بيئة الإنتاج إلى Process Monitor يبقي عملية Horizon قيد التشغيل.
QUEUE_CONNECTION=redis

يستخدم Horizon اتصال Redis داخليًا اسمه horizon. الاسم محجوز، فلا تستخدمه اسمًا لاتصال آخر في config/database.php ولا قيمة لخيار use داخل إعداد Horizon.

تثبيت Laravel Horizon وإعداده

تثبيت الحزمة

composer require laravel/horizon

نشر ملفات Horizon

php artisan horizon:install

ينشر الأمر ملف config/horizon.php وموفر الخدمة والأصول اللازمة.

تشغيل Horizon

php artisan horizon

ثم افتح:

https://example.com/horizon

إعداد بيئات التشغيل

'environments' => [
    'production' => [
        'supervisor-default' => [
            'maxProcesses' => 10,
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
        ],
    ],

    'local' => [
        'supervisor-default' => [
            'maxProcesses' => 3,
        ],
    ],
],

يختار Horizon الإعداد المطابق عادة لقيمة APP_ENV. تأكد من وجود بيئة لكل قيمة تستخدمها، ويمكن تعريف * كإعداد احتياطي إذا لم يوجد تطابق مباشر.

فهم Supervisors وWorkers والطوابير

Supervisor داخل Horizon هو مجموعة إعدادات تشرف على عدد من Worker processes وتحدد اتصال Redis والطوابير واستراتيجية الموازنة وحدود الموارد والمحاولات. يمكن للبيئة الواحدة أن تضم عدة Supervisors.

'defaults' => [
    'supervisor-default' => [
        'connection' => 'redis',
        'queue' => ['default', 'notifications'],
        'balance' => 'auto',
        'autoScalingStrategy' => 'time',
        'minProcesses' => 1,
        'maxProcesses' => 10,
        'tries' => 3,
        'timeout' => 60,
    ],
],

متى تستخدم عدة Supervisors؟

  • عندما تحتاج طابورًا عالي الأولوية بموارد محجوزة.
  • عندما تستهلك Jobs معينة CPU أو ذاكرة كبيرة وتريد تحديد عددها.
  • عندما تختلف قيمة timeout أو tries بين مجموعات المهام.
  • عندما تريد Scaling مستقلًا لكل طابور.

وضع الصيانة

لا يعالج Horizon المهام أثناء Maintenance Mode افتراضيًا. لتجاوز ذلك لمسؤول محدد:

'force' => true,

فعّل الخيار فقط إذا كانت المهام آمنة أثناء نشر التطبيق أو صيانته.

المحاولات والمهلة وBackoff وإدارة الذاكرة

عدد المحاولات tries

'tries' => 3,

إذا لم تضبط tries يستخدم Horizon محاولة واحدة افتراضيًا، إلا إذا عرّفت Job خاصية $tries التي لها الأولوية. القيمة 0 تعني محاولات غير محدودة؛ استخدم معها $maxExceptions لمنع حلقة فشل لا تنتهي.

Middleware مثل WithoutOverlapping وRateLimited قد تستهلك Attempts من دون تنفيذ منطق Job كاملًا، لذلك لا تضبط tries بصورة عشوائية.

مهلة التنفيذ timeout

'timeout' => 60,

اجعل Horizon timeout أكبر من أي Job-level timeout، وأقصر بعدة ثوانٍ من retry_after في config/queue.php. وإلا قد تُقتل Job أثناء التنفيذ أو تُعالج مرتين.

Backoff وإعادة المحاولة

'backoff' => [1, 5, 10],

ينتظر Horizon ثانية قبل الإعادة الأولى، ثم 5 ثوانٍ، ثم 10 ثوانٍ، ويستمر باستخدام آخر قيمة للمحاولات الإضافية. يقلل Exponential backoff الضغط على خدمة خارجية متعثرة.

إعادة تدوير Workers

'memory' => 128,
'maxJobs' => 1000,
'maxTime' => 3600,
'sleep' => 3,
'rest' => 0,
'nice' => 0,
  • memory: الحد الأقصى بالميغابايت قبل إعادة Worker.
  • maxJobs: عدد Jobs قبل إعادة التشغيل؛ الصفر يعطل الحد.
  • maxTime: مدة حياة Worker بالثواني؛ الصفر يعطل الحد.
  • sleep: الانتظار عندما لا توجد مهمة.
  • rest: توقف بين كل مهمتين.
  • nice: أولوية جدولة العملية في النظام.

استراتيجيات موازنة الطوابير

يدعم Horizon ثلاث قيم لخيار balance: auto وsimple وfalse.

Auto Balancing

يوزع Horizon العمليات ديناميكيًا وفق حمل كل Queue. يمكن الاستناد إلى الزمن المتوقع لتفريغ الطابور باستخدام time أو إلى عدد Jobs باستخدام size.

'connection' => 'redis',
'queue' => ['default', 'notifications'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'minProcesses' => 1,
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,

يسمح المثال بإضافة أو إزالة عملية واحدة كل ثلاث ثوانٍ. اضبط سرعة التغيير بعناية حتى لا تتأرجح العمليات بسرعة أو تتأخر في امتصاص موجة كبيرة.

ترتيب الطوابير في استراتيجية auto لا يفرض أولوية صارمة. إذا احتجت أولوية أو موارد مضمونة، افصل الطوابير في Supervisors مستقلة وحدد لكل منها minProcesses وmaxProcesses.

Simple Balancing

'queue' => ['default', 'notifications'],
'balance' => 'simple',
'processes' => 10,

يقسم Horizon عدد العمليات الثابت بالتساوي؛ في المثال تحصل كل Queue على خمس عمليات.

No Balancing

'queue' => ['high', 'default'],
'balance' => false,
'minProcesses' => 1,
'maxProcesses' => 10,

يعالج Horizon الطوابير حسب ترتيبها. لن ينتقل إلى default قبل إنهاء ما ينتظر في high، مع قدرته على تغيير إجمالي عدد Workers ضمن الحدود.

عزل المهام كثيفة الموارد

'production' => [
    'supervisor-default' => [
        'queue' => ['default'],
        'minProcesses' => 1,
        'maxProcesses' => 10,
    ],
    'supervisor-images' => [
        'queue' => ['images'],
        'minProcesses' => 1,
        'maxProcesses' => 2,
        'memory' => 512,
        'timeout' => 300,
    ],
],

تشغيل Horizon وإدارته

الأمروظيفته
php artisan horizonتشغيل العملية وجميع Supervisors للبيئة.
php artisan horizon:statusعرض حالة Horizon.
php artisan horizon:pauseإيقاف المعالجة مؤقتًا.
php artisan horizon:continueاستئناف المعالجة.
php artisan horizon:terminateإنهاء سلس بعد إكمال المهام الحالية.
php artisan horizon:pause-supervisor supervisor-1إيقاف Supervisor محدد.
php artisan horizon:continue-supervisor supervisor-1استئناف Supervisor محدد.
php artisan horizon:supervisor-status supervisor-1التحقق من حالة Supervisor.

إعادة التحميل في التطوير

يدعم Horizon أمر horizon:listen بعد تثبيت Chokidar ووجود Node:

npm install --save-dev chokidar
php artisan horizon:listen

داخل Docker أو Vagrant يمكن استخدام:

php artisan horizon:listen --poll

نشر Horizon باستخدام Supervisor

شغل Horizon في الإنتاج تحت مدير عمليات يعيد تشغيله إذا توقف. مثال Supervisor على Ubuntu:

[program:horizon]
process_name=%(program_name)s
command=php /home/forge/example.com/artisan horizon
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/home/forge/example.com/horizon.log
stopwaitsecs=3600

يجب أن تكون stopwaitsecs أطول من أطول Job؛ وإلا قد يقتل Supervisor المهمة قبل اكتمالها.

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start horizon

ضمن عملية النشر

php artisan horizon:terminate

يكمل Horizon المهام الحالية ثم يتوقف، ويعيد مدير العمليات تشغيله بالكود الجديد. لا تكتفِ بنشر الملفات؛ فالعمال عمليات طويلة العمر لا تقرأ الكود الجديد تلقائيًا.

حماية لوحة Horizon في الإنتاج

تتوفر اللوحة في /horizon وتكون متاحة افتراضيًا في البيئة المحلية فقط. عدّل Gate viewHorizon داخل HorizonServiceProvider:

protected function gate(): void
{
    Gate::define('viewHorizon', function (User $user) {
        return $user->is_admin === true;
    });
}
  • اضبط APP_ENV=production وAPP_DEBUG=false.
  • استخدم Role أو Permission مخصصًا للمراقبة.
  • أضف VPN أو Zero Trust أو IP allowlist عند الإمكان.
  • لا تجعل حماية الشبكة بديلًا عن Gate إلا ضمن تصميم مصادقة مدروس.
  • أضف CSP nonce عبر Horizon::cspNonce() إذا كانت سياسة CSP تتطلب ذلك.

الوسوم وكتم المهام

يضيف Horizon وسومًا تلقائيًا عند وجود Eloquent Models داخل خصائص Job. تمرير نموذج Video بمعرف 1 قد ينتج الوسم App\Models\Video:1، ما يسهل البحث عن كل المهام المتعلقة به.

وسوم مخصصة

public function tags(): array
{
    return [
        'tenant:'.$this->tenantId,
        'report:'.$this->reportId,
    ];
}

كتم Jobs كثيرة الضجيج

'silenced' => [
    App\Jobs\Heartbeat::class,
],

'silenced_tags' => [
    'notifications',
],

يمكن كذلك تطبيق Laravel\Horizon\Contracts\Silenced على Job. الكتم ينظف واجهة Completed Jobs، لكنه ليس معالجة لمشكلة أداء أو فشل.

Metrics والإشعارات

تعرض لوحة Horizon Throughput وRuntime ومقاييس Jobs وQueues. حتى تظهر الرسوم التاريخية، جدول أمر Snapshot:

use Illuminate\Support\Facades\Schedule;

Schedule::command('horizon:snapshot')->everyFiveMinutes();

تأكد من تشغيل Laravel Scheduler على الخادم.

إشعارات وقت الانتظار الطويل

يستطيع Horizon إرسال إشعار عندما يتجاوز انتظار Queue حدًا معينًا، عبر قنوات مدعومة مثل البريد وSlack وSMS وفق إعداد التطبيق. اضبط حدود الانتظار لكل Connection وQueue في config/horizon.php، ثم سجل قنوات الإشعار في موفر Horizon بحسب التوثيق الرسمي.

المهام الفاشلة وتنظيف الطوابير

افحص Exception وPayload وعدد المحاولات قبل إعادة Job. إعادة كل المهام بلا فهم السبب قد تكرر آثارًا جانبية مثل إرسال بريد مرتين أو خصم مالي متكرر. صمم Jobs المهمة لتكون Idempotent قدر الإمكان.

php artisan horizon:forget 5
php artisan horizon:forget --all

لحذف Failed Job من Redis عند استخدام Horizon، استخدم horizon:forget بدل queue:forget.

تنظيف Queue

php artisan horizon:clear
php artisan horizon:clear --queue=emails

أوامر المسح مدمرة للمهام المنتظرة. حدد الاتصال والطابور بدقة، وافحص أثر فقدان Jobs قبل التنفيذ في الإنتاج.

سيناريوهات عملية

تراكم آلاف الإشعارات

  1. راجع Wait time وThroughput لطابور notifications.
  2. تأكد من أن Horizon يعمل وأن Workers لا تنهار.
  3. افصل notifications في Supervisor مستقل.
  4. ارفع maxProcesses ضمن قدرة Redis والخادم ومزود البريد.
  5. استخدم Rate Limiting إذا كان المزود الخارجي يفرض حدودًا.

Job معالجة صور تستهلك CPU

  1. انقلها إلى Queue اسمها images.
  2. خصص Supervisor بحد أقصى صغير للعمليات.
  3. ارفع memory وtimeout بما يناسب الملف الأقصى.
  4. أعد تدوير Workers عبر maxJobs أو maxTime.
  5. راقب زمن التنفيذ والذاكرة قبل زيادة التوازي.

Job تُعالج مرتين

  1. تحقق من أن timeout أقصر من retry_after.
  2. راجع مدة Job الفعلية والاتصالات الخارجية.
  3. اجعل العملية Idempotent باستخدام مفتاح عمل فريد أو قفل مناسب.
  4. راجع WithoutOverlapping وunique jobs والمحاولات.

تغييرات الكود لا تظهر

  1. تذكر أن Horizon عملية طويلة العمر.
  2. نفذ php artisan horizon:terminate أثناء النشر.
  3. تأكد من أن Supervisor أعاد تشغيل العملية.
  4. راجع Working directory ومستخدم النظام ومسار PHP.

المشكلات الشائعة وحلولها

لوحة Horizon تعمل لكن Jobs لا تُعالج

  • تأكد من QUEUE_CONNECTION=redis.
  • راجع أسماء Connection وQueue في Job وSupervisor.
  • تحقق من Maintenance Mode وخيار force.
  • نفذ php artisan horizon:status.
  • راجع Redis والLogs وSupervisor.

Horizon لا يستخدم إعداد البيئة

  • طابق APP_ENV مع مفتاح environments.
  • أضف إعداد * احتياطيًا عند الحاجة.
  • امسح وأعد بناء Config cache.
  • أنه Horizon وأعد تشغيله بعد تغيير الإعداد.

Jobs تفشل بسبب timeout

  • قارن Horizon timeout وJob timeout وretry_after.
  • لا ترفع الحدود قبل تحليل سبب البطء.
  • قسم Job الكبيرة أو حسّن Query والخدمة الخارجية.
  • اضبط stopwaitsecs في Supervisor بما يتجاوز أطول Job.

استهلاك الذاكرة يزداد

  • اضبط memory وmaxJobs وmaxTime لإعادة تدوير Workers.
  • حرر موارد كبيرة داخل Job ولا تحتفظ بحالة ساكنة.
  • استخدم chunking وstreaming عند معالجة بيانات ضخمة.
  • اعزل Jobs الثقيلة في Supervisor مستقل.

الأسئلة الشائعة

هل Horizon يعمل مع Database Queue؟

لا. يتطلب Horizon Redis لتشغيل الطوابير التي يديرها.

هل Horizon بديل عن Supervisor؟

لا. Horizon يدير Laravel Workers داخليًا، بينما Supervisor أو systemd يراقب عملية Horizon نفسها ويعيد تشغيلها إذا توقفت أو بعد النشر.

هل Auto Balancing يعني أولوية حسب ترتيب Queue؟

لا. يوزع Auto العمال حسب الحمل والاستراتيجية. استخدم Supervisors مستقلة للموارد المضمونة، أو balance=false إذا احتجت ترتيب معالجة صارمًا.

لماذا يجب إنهاء Horizon عند النشر؟

لأن Workers طويلة العمر تحتفظ بالكود المحمل. الإنهاء السلس يسمح لمدير العمليات بتشغيل نسخة جديدة.

هل يمكن تشغيل عدة Horizon instances؟

نعم ضمن بنية Redis مناسبة وإعدادات موارد مدروسة. استخدم Prefix وبيئات واتصالات صحيحة، وراقب مجموع Workers عبر الخوادم حتى لا تتجاوز قدرة قواعد البيانات والخدمات الخارجية.

خلاصة أفضل الممارسات

  1. افصل الطوابير حسب الأولوية وكلفة الموارد.
  2. اجعل timeout أقصر من retry_after وأطول من Job timeout.
  3. استخدم Backoff عند التعامل مع خدمات خارجية.
  4. صمم Jobs الحساسة لتكون Idempotent.
  5. أعد تدوير Workers لحماية العمليات طويلة العمر من تسرب الذاكرة.
  6. استخدم Supervisors مستقلة عندما تحتاج موارد مضمونة.
  7. شغل Horizon تحت Process Monitor.
  8. نفذ horizon:terminate ضمن كل نشر.
  9. احمِ لوحة Horizon بـGate وقيود شبكة.
  10. جدول horizon:snapshot واضبط تنبيهات الانتظار.