معالجة المهامّ الخلفية في Laravel بعمق: الطوابير وإعادة المحاولة والحصانة التكرارية
دليل إنتاجيّ عميق لطوابير Laravel: تصميم المهامّ الصغيرة، التراجع الأسّيّ والارتجاف لمنع عاصفة إعادة المحاولة، الحصانة التكرارية للعمليات الحرجة، والفشل غير الصامت. بأمثلة كود.
الساعة الثالثة فجرًا، ومنصّة الدفع تنهار. لا هجوم، ولا عطل في الخادم. السبب مهمّة واحدة في الطابور: استدعاء لبوّابة دفع خارجية تأخّر لثوانٍ، ففشلت المهمّة، فأعادت المحاولة، ثمّ فشلت آلاف المهامّ الأخرى معها دفعةً واحدة، فأغرقت قاعدة البيانات بحالات جمود (Deadlock). هذه «عاصفة إعادة المحاولة» (Retry Storm)، أحد أخطر ما يواجه أنظمة الطوابير في الإنتاج. والمفارقة أن الطوابير وُجدت لتحمي تطبيقك، لكنها حين تُصمَّم بسذاجة تتحوّل إلى مصدر الكارثة نفسها. لنبنِ طوابير لا تنهار.
لماذا الطوابير أصلًا؟
الفكرة الأساسية بسيطة: أيّ عمل بطيء لا يحتاج المستخدم انتظار نتيجته — إرسال بريد، معالجة صورة، استدعاء واجهة خارجية — يُنقَل خارج دورة طلب HTTP إلى «مهمّة» (Job) تُنفَّذ في الخلفية عبر «عامل» (Worker). النتيجة: يبقى التطبيق سريع الاستجابة، ويُنجَز العمل الثقيل بعيدًا عن نظر المستخدم. لكنّ هذا التصميم يفتح بابًا جديدًا: ماذا يحدث حين تفشل المهمّة؟ وهنا يبدأ الفرق بين طابور هاوٍ وآخر إنتاجيّ.
القاعدة الأولى: اجعل مهامّك صغيرة وأحادية المسؤولية
أكبر خطأ معماريّ هو المهمّة «الوحش» التي تفعل كل شيء: تتحقّق، وتنشئ فاتورة، وتخصم رصيدًا، وترسل إشعارًا، في دالّة واحدة. حين تفشل عند الخطوة الرابعة، تُعاد المحاولة من الأولى، فتتكرّر الخطوات الثلاث الأولى بآثارها الجانبية. القاعدة: مهمّة واحدة تفعل شيئًا واحدًا. قسّم العملية الكبيرة إلى سلسلة مهامّ صغيرة (تحقّق، إنشاء، إشعار)، كلٌّ قابلة لإعادة المحاولة والاختبار والمراقبة وحدها. المهامّ الصغيرة تجعل إعادة المحاولة رخيصةً وآمنة.
إعادة المحاولة والـ Backoff: من يمنع العاصفة
يوفّر Laravel إعادة محاولة تلقائية عبر خاصّية tries، لكنّ الإعداد الافتراضيّ نقطة بداية لا نهاية. المشكلة أن إعادة المحاولة الفورية المتكرّرة هي وقود «العاصفة». الحلّ هو التراجع المتدرّج (Backoff). أبسطه ثابت، لكنّ الأقوى هو «التراجع الأسّيّ» (Exponential): ضاعِف التأخير بعد كل محاولة، لتمنح الخدمة الخارجية وقتًا للتعافي:
class ProcessPayment implements ShouldQueue
{
use Queueable;
public $tries = 4;
// تأخير متدرّج: 5 ثوانٍ، ثم 15، ثم 60
public function backoff(): array
{
return [5, 15, 60];
}
}حين تتجاوز المحاولاتُ عددَ قيم المصفوفة، يستخدم Laravel القيمة الأخيرة للبقية. لكن يبقى خطرٌ خفيّ: إن فشلت آلاف المهامّ في اللحظة نفسها، فستعيد المحاولة كلها في اللحظة نفسها أيضًا — عاصفة متزامنة. الحلّ الاحترافيّ إضافة «الارتجاف» (Jitter): عشوائية صغيرة على التأخير تبعثر المحاولات فتكسر التزامن. وللتعامل مع خدمة كثيرة الأعطال، استخدم وسيط ThrottlesExceptions الذي يوقف الطرق مؤقّتًا بعد عدد من الأخطاء.
القاعدة الأهمّ على الإطلاق: الحصانة التكرارية (Idempotency)
هنا جوهر المقال. في الأنظمة الموزّعة، يجب أن تفترض أن كل مهمّة قد تُنفَّذ أكثر من مرّة — بسبب إعادة المحاولة، أو عاملٍ قُتِل في منتصف العمل ثم أُعيدت المهمّة للطابور. السبب الشائع: سوء ضبط retry_after مقابل مهلة العامل (timeout). فإن قُتِلت مهمّة وأُعيدت، ستعمل مرّتين ما لم يمنع تصميمك التكرار. تخيّل مهمّة تخصم من بطاقة عميل تُنفَّذ مرّتين: خصمٌ مزدوج، وعميل غاضب.
«الحصانة التكرارية» (Idempotency) تعني أن تنفيذ العملية نفسها عدّة مرّات ينتج الأثر نفسه كأنها نُفّذت مرّة. الطريقة العملية: خزّن «مفتاح عملية» (Operation Key) في تخزين دائم قبل تنفيذ الأثر الجانبيّ، وتحقّق منه في البداية:
public function handle(): void
{
// تحقّق أوّلًا: هل عولجت هذه العملية من قبل؟
$exists = ProcessedOperation::where(
'idempotency_key', $this->paymentId
)->exists();
if ($exists) {
return; // عولجت سابقًا — اخرج بأمان
}
// نفّذ الأثر الجانبيّ داخل معاملة
DB::transaction(function () {
$this->chargeCustomer();
ProcessedOperation::create([
'idempotency_key' => $this->paymentId,
'processed_at' => now(),
]);
});
}الأهمّ أن الضمان يعيش في تخزين دائم (قيد فريد في قاعدة البيانات، أو مفتاح idempotency يدعمه المزوّد الخارجيّ)، لا في الذاكرة ولا في سجلّ «أفضل جهد». وإن كانت الواجهة الخارجية تدعم «مفتاح التكرار» (Idempotency-Key)، فمرّره إليها ليتعرّف على المحاولات المكرّرة. Laravel يقدّم أيضًا عقد ShouldBeUnique لمنع جدولة نسخ متعدّدة من المهمّة نفسها أصلًا.
حدود المعاملة: أطلق المهمّة بعد الالتزام
فخّ إنتاجيّ دقيق: إن أطلقت مهمّةً تعتمد على بيانات داخل معاملة قاعدة بيانات (transaction) قبل أن تُلتزَم (commit)، فقد يلتقطها العامل السريع قبل أن تُكتَب البيانات فعلًا، فيجدها غير موجودة. الحلّ استخدام dispatchAfterResponse للمهامّ الخفيفة، أو الأضمن: افصل الإطلاق ليحدث بعد الالتزام عبر afterCommit، إمّا في إعداد الاتّصال أو صراحةً:
ProcessPayment::dispatch($payment)->afterCommit();هذا يضمن ألّا يبدأ العامل قبل أن تصبح البيانات التي يعتمد عليها متاحةً فعلًا في قاعدة البيانات.
الفشل غير الصامت: أخطر ما يُهمَل
حين تفشل مهمّة نهائيًّا بعد استنفاد محاولاتها، يخزّنها Laravel في جدول failed_jobs مع حمولتها ورسالة الخطأ وأثر المكدّس — كنزٌ للتشخيص. لكنّ الخطر الأكبر ليس الفشل، بل الفشل «الصامت»: استثناء يُبتَلع في سجلّ بدل أن يتحوّل إلى حالة عمل دائمة. تخيّل مهمّة تزامن فاتورة مع نظام محاسبة تفشل، فتصير سطرًا في سجلّ لا يراه أحد، بينما يظنّ النظام أن التزامن تمّ. القاعدة الذهبية: اجعل الفشل الحقيقيّ مرئيًّا. طبّق دالّة failed لالتقاط الفشل النهائيّ واتخاذ إجراء (تنبيه، أو تعويض)، وراقب عدد المهامّ الفاشلة بتنبيهات لها مالكٌ مسؤول:
public function failed(?Throwable $exception): void
{
// تنفَّذ بعد فشل كل المحاولات
Notification::route('slack', config('alerts.ops'))
->notify(new JobFailedAlert($this->paymentId, $exception));
// سجّل حالة عمل دائمة للمصالحة لاحقًا
PaymentReconciliation::create([
'payment_id' => $this->paymentId,
'status' => 'failed',
]);
}الفصل والمراقبة: طوابير متعدّدة لا طابور واحد
خطأ شائع في التوسّع: خلط كل شيء في طابور واحد. مزجُ عمليات الاستيراد الثقيلة مع أعمال الفوترة الحرجة مع المهامّ الحسابية يجعل الكمون (Latency) غير متوقّع: مهمّة استيراد ضخمة قد تعطّل إشعار دفع عاجلًا. القاعدة: افصل الأحمال في طوابير مخصّصة بأولويات وعمّال منفصلين (critical، imports، compute). ومع Laravel Horizon (لسائق Redis)، تحصل على لوحة مراقبة توضّح عمق التراكم (Backlog) ومعدّل الفشل وتوزيع أزمنة التنفيذ. راقب المؤشّرات الحرجة: عمق الطابور، معدّل الفشل (قفزته تعني انحدارًا)، والكمون عند المئين 95 و99، وإعادة تشغيل العمّال (تعني تسريب ذاكرة أو مهامّ ضخمة).
القاعدة العملية التي تختصر كل ما سبق: لا تصمّم طابورك على افتراض النجاح، بل على افتراض الفشل. المهمّة الجيّدة صغيرة، وقابلة لإعادة المحاولة بأمان لأنها حصينة تكراريًّا، وتتراجع بتدرّج وارتجاف كي لا تسبّب عاصفة، وتفشل بصوتٍ مسموع لا في صمت. الطوابير في جوهرها ليست أداة «أطلقها وانسها»، بل نظامٌ يتطلّب تصميمًا واعيًا لكل مسار فشل. من يبني بهذا الوعي، ينام مطمئنًّا حين تتأخّر بوّابة الدفع في الثالثة فجرًا — لأن نظامه صُمِّم لتلك اللحظة تحديدًا.
هل وجدت هذا المقال مفيدًا؟
النشرة البريدية
أعجبك المحتوى؟
اشترك ليصلك كل جديد من المقالات والأخبار في بريدك مباشرةً.