Laravel 13.34: احتساب انهيار الـ Worker كاستثناء في الـ Queue

Laravel 13.34 يحتسب انهيار الـ worker في الـ queue (نفاد الذاكرة أو segfault) كاستثناء ضمن maxExceptions، ويضيف إشعار المهلة وقياس مدة الـ job.

ما الذي صدر في Laravel 13.34؟

بحسب ملاحظات الإصدار على GitHub، صدر Laravel Framework v13.34.0 في 29 سبتمبر 2026 ويضم نحو 45 pull request. وكتب Laravel News عن الإصدار في 30 سبتمبر 2026. أبرز التغييرات تخص الـ queue:

  • احتساب انهيار الـ worker ضمن maxExceptions عبر CountCrashesAsExceptions.
  • إشعار الـ job بإشارة SIGALRM قبل انتهاء المهلة ليحفظ تقدمه.
  • إضافة خاصية duration إلى حدث JobProcessed.
  • إضافة خيار --queue إلى الأمر queue:flush.

وفي الإصدار أيضًا إصلاحات متفرقة: Schema::getColumn() لجلب تفاصيل عمود واحد، وإصلاح قفل Memcached الذي كان ينتهي مبكرًا عند المدد الطويلة، وإصلاحات في Collection و Blade و DatabaseLock.

المشكلة: job يقتل الـ worker ولا يُحتسب فشلًا

لنبدأ بسؤال: ماذا يحدث لـ job تستهلك ذاكرة أكثر من المسموح فيقتلها النظام؟ الـ worker نفسه يموت، فلا يصل Laravel إلى لحظة التقاط الاستثناء. وبحسب توثيق الـ queues، فإن انهيار الـ worker لا يُحتسب افتراضيًا ضمن MaxExceptions.

النتيجة العملية: job معيبة قد تقتل الـ worker في كل محاولة، ويعيد المشرف (Supervisor مثلًا) تشغيله، فتعود الـ job وتقتله من جديد، حتى تستنفد Tries كلها وهي قد تكون 25 محاولة. حدّ maxExceptions الذي وضعته لحمايتك لم يتدخل أصلًا.

الحل: CountCrashesAsExceptions

الإصدار الجديد يتيح للـ job أن تطلب احتساب الانهيار كاستثناء. حسب التوثيق:

#[Tries(25)]
#[MaxExceptions(3)]
#[CountCrashesAsExceptions]
class ProcessPodcast implements ShouldQueue
{
    // ...
}

وبحسب Laravel News يوجد بديل بالخاصية: public $countCrashesAsExceptions = true;.

كيف يعمل؟

يصف التوثيق الآلية هكذا: الـ worker يخزّن علامة (marker) في الـ cache أثناء معالجة الـ job، فإذا وجد العلامة ما زالت موجودة عند المحاولة التالية، اعتبر المحاولة السابقة استثناءً. أي أن الـ worker يكتب مفتاحًا عند بدء job مفعّل لها الخيار، ويحذفه عند انتهاء المحاولة بشكل طبيعي. إن مات الـ worker فبقي المفتاح، عُرف أن المحاولة السابقة انتهت بانهيار.

لهذا الأمر نتيجتان يجب الانتباه لهما:

  • الميزة اختيارية لكل job، فلن يتغير سلوك أي job قائمة حتى تفعّلها.
  • تعتمد على الـ cache، فتأكد أن مخزن الـ cache الذي تستخدمه مشترك بين الـ workers وثابت بما يكفي.

إشعار الـ job قبل انتهاء المهلة

كما يعرف من استخدم --timeout أو Timeout، فإن مهلة الـ job تعتمد على امتداد PCNTL وإشارة SIGALRM، وعند انتهائها تستهلك الـ job محاولة وتُعاد للطابور افتراضيًا (ما لم تستخدم FailOnTimeout).

الجديد في 13.34 أن الـ job القابلة للمقاطعة (interruptible) تتلقى الإشارة قبل أن تُعلَّم فاشلة، فتستطيع حفظ تقدمها. المثال المنشور في Laravel News:

public function interrupted(int $signal): void
{
    if ($signal === SIGALRM) {
        $this->saveProgress();
    }
}

ويطلق الـ worker أيضًا حدث JobInterrupted. فائدة هذا واضحة في المهام الطويلة كاستيراد الملفات أو المزامنة: بدل البدء من الصفر في المحاولة التالية، تحفظ آخر موضع وصلت إليه. راجع الـ PR رقم #61651 لمعرفة كيفية تفعيل الـ job كقابلة للمقاطعة بالتحديد، فلم أتحقق من تفاصيل ذلك بنفسي.

قياس مدة الـ job بلا كود إضافي

كان قياس زمن الـ job يتطلب الاستماع إلى JobProcessing و JobProcessed وحفظ الوقت بينهما. الآن يحمل JobProcessed الخاصية duration بالميلي ثانية، وهي رقم عشري مقرّب إلى منزلتين:

Event::listen(function (JobProcessed $event) {
    Log::info("{$event->job->resolveName()} took {$event->duration}ms");
});

(الـ PR: #61672). مناسبة لإرسال المدد إلى أداة مراقبة دون أي تتبّع يدوي.

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

الأمر queue:flush كان يمسح جدول الـ failed jobs كله. الآن يمكن تحديد طابور:

php artisan queue:flush --queue=sftp-sync
php artisan queue:flush --queue=sftp-sync --hours=48

هذا يفيدك عندما تفشل مزامنة معينة بسبب مشكلة عابرة وتريد تنظيفها دون فقدان سجل أخطاء الطوابير الأخرى التي تحتاج فحصًا.

مثال عملي: job تستهلك الذاكرة

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

مع #[MaxExceptions(3)] و #[CountCrashesAsExceptions] تُحتسب كل محاولة انتهت بانهيار كاستثناء، فتُعلَّم الـ job فاشلة بعد ثلاث محاولات بدل استنزاف كل المحاولات، وتظهر في جدول الـ failed jobs لتفحصها وتعالج السبب الحقيقي، كتقسيم التقرير إلى دفعات أو استخدام LazyCollection.

تنبيهات قبل الترقية والاعتماد

  • ميزات الـ queue أعلاه إضافية ولا يُفترض أن تغيّر سلوك job لم تفعّلها، لكن اقرأ ملاحظات الإصدار كاملة قبل الترقية.
  • ذُكر في الإصدار أن العلاقات polymorphic تفرض الآن morph maps في عمليات القراءة. إن كان تطبيقك يخزّن أسماء كلاسات كاملة في أعمدة النوع دون تسجيل morph map، اختبر ذلك جيدًا، ولم أتحقق من تفاصيل هذا التغيير في الـ PR.
  • الإشارات (SIGALRM) تتطلب امتداد PCNTL، وهو غير متاح على Windows عمومًا.

كيف تتبنّاها خطوة بخطوة

  1. حدّد الـ jobs الثقيلة التي سبق أن قتلت الـ worker (استيراد، معالجة صور، تصدير تقارير).
  2. أضف إليها #[MaxExceptions(...)] و #[CountCrashesAsExceptions].
  3. تأكد من أن الـ cache مشترك بين الـ workers.
  4. في الـ jobs الطويلة، احفظ التقدم عند مقاطعة المهلة.
  5. استمع إلى duration وسجّله لتكتشف الـ jobs البطيئة مبكرًا.
  6. استخدم queue:flush --queue=... لتنظيف طابور واحد.

الخلاصة

الإصدار 13.34 يغلق ثغرة صغيرة لكنها مؤلمة في الإنتاج: job تقتل الـ worker مرارًا دون أن يوقفها maxExceptions. فعّل CountCrashesAsExceptions على الـ jobs الثقيلة، واحفظ تقدم المهام الطويلة عند المقاطعة، وسجّل duration لتراقب أداء طوابيرك.

المصادر