مهامك في Laravel ستُنفَّذ مرتين. صمّمها على ذلك.
التسليم مرة واحدة على الأقل يعني أن كل مهمة في طابور تُنفَّذ في النهاية أكثر من مرة. ماذا يكسر ذلك، ولماذا ليست إعادة المحاولة السبب الوحيد، وكيف تحرس مهمة دون سباق.
يمنحك طابور Laravel تسليمًا مرة واحدة على الأقل. وتقرأ أغلب الفرق ذلك على أنه "يعيد المحاولة عند الفشل" وتمضي، وهذا نصفه. والنصف الآخر أن المهمة التي نجحت قد تُنفَّذ مرة أخرى، ولا قدر من معالجة الأخطاء الحذرة يمنع ذلك.
أربع طرق تُنفَّذ بها مهمة مرتين
إعادة المحاولة التي ضبطتها. ترمي المهمة استثناءً فيعيدها الطابور. بديهي، وهي الوحيدة التي يحسب الجميع حسابها.
المهلة المضبوطة خطأ. تستغرق المهمة أطول من المهلة المضبوطة. يُقتل العامل، ولا تُؤكَّد الرسالة، فيلتقطها عامل آخر - بينما قد يكون الأول لا يزال ينهي عمله.
النشر. يكون عامل ممسكًا بمهمة حين تتغير الشيفرة تحته. فيُعاد تشغيله وتعود الرسالة إلى الطابور. أدّت المهمة أغلب عملها قبل إعادة التشغيل، وسيُؤدّى كله مرة أخرى.
البنية التحتية. ينقطع اتصال بين "انتهت المهمة" و"وصل التأكيد إلى الوسيط". حدث العمل. والطابور لا يعلم بذلك.
الأولى وحدها تحت سيطرتك. والثلاث الأخرى هي سبب كون الطابور مرة واحدة على الأقل لا مرة واحدة بالضبط، وسبب أن التسليم مرة واحدة بالضبط ليس شيئًا يمكنك ضبطه.
كم يكلّف هذا فعلًا
رسالة تُرسل مرتين إحراج. وكل ما تحت أسوأ:
- دفعة تُحصَّل مرتين، وهي استرداد وخيط دعم وعميل لا يعود.
- webhook يُسلَّم مرتين إلى شريك نظامه هو أيضًا يتأثر بالتكرار.
- مستوى مخزون يُنقَص مرتين، وهو منتج بيع زيادةً.
- رقم فاتورة يُخصَّص مرتين، وهو في بعض الولايات القضائية مشكلة محاسبية بجهة تنظيمية ملحقة بها.
والأخير يكفّ عن كونه افتراضيًا لحظة يبدأ طابور بدفع سجلات إلى دفتر أستاذ شخص آخر. فالمهمة المكررة على واجهة محاسبية فاتورة مكررة في دفاتر حقيقية، ومن سيفكّها لا يعمل عندك.
النمط نفسه في كلها: وقع الأثر خارج قاعدة بياناتك، فلم تستطع معاملة التراجع عنه.
الإصلاح الذي لا يعمل
هذه هي الشيفرة التي نجدها أكثر من غيرها، وهي سباق:
public function handle(): void
{
if ($this->order->refresh()->paid_at !== null) {
return; // تم بالفعل، تخطَّ
}
$charge = $this->gateway->charge($this->order); // ← كلا العاملين يصل هنا
$this->order->update(['paid_at' => now()]);
}يقرأ عاملان الصف قبل أن يكتبه أيّهما. يرى كلاهما null، فيسحب كلاهما. الفحص ثم
الفعل ليس حارسًا بل نافذة أضيق - وتكرارات الطابور تصل في الظروف نفسها التي تُضيّق
النوافذ.
حراسات تصمد
القاعدة أن التفرّد يجب أن يفرضه شيء لا يمكن مسابقته: قاعدة البيانات، أو الطرف الثالث.
قيد تفرّد يؤدي العمل. دع الإدراج يفشل بدل السؤال أولًا:
public function handle(): void
{
try {
$payment = Payment::create([
'order_id' => $this->order->id,
'idempotency_key' => $this->key, // فهرس فريد
]);
} catch (UniqueConstraintViolationException) {
return; // عامل آخر يملك هذه
}
$this->gateway->charge($this->order, $this->key);
}مفتاح عدم تكرار يفرضه المزوّد. كل واجهة دفع جادّة تقبل واحدًا. طلبان متطابقان بالمفتاح نفسه ينتجان سحبًا واحدًا واستجابتين متطابقتين. وهذا أقوى ضمان متاح، لأنه يصمد حتى لو كانت قاعدة بياناتك أنت هي ما فشل.
تحديث مشروط. اجعل انتقال الحالة نفسه هو القفل:
$claimed = Order::where('id', $this->order->id)
->whereNull('paid_at')
->update(['paid_at' => now()]); // يعيد عدد الصفوف المتأثرة
if ($claimed === 0) {
return; // نقلها غيرك
}عبارة واحدة، تقررها قاعدة البيانات. والعامل الذي يحصل على 1 يملك العمل.
مفتاح يُولَّد عند الإرسال لا داخل المهمة. هذا مهم ويسهل إغفاله: إن حسبت المهمة مفتاح عدم التكرار من الوقت الحالي أو من قيمة عشوائية، أنتج تنفيذان للمهمة نفسها مفتاحين ولم تعمل الحراسة أبدًا. المفتاح جزء من حمولة المهمة، يُنشأ مرة، حين تُرسَل المهمة.
وما دمت هناك
ضبطان وعادة، وكلها رخيصة:
اضبط المهلة تحت تأخير إعادة المحاولة. إن كانت المهمة قد تعمل 90 ثانية وتُعاد بعد 60، فقد ضمنت لنفسك تنفيذين متزامنين للمهمة نفسها.
استخدم تباعدًا لا تأخيرًا ثابتًا. public $backoff = [10, 60, 300]; -
انقطاع لدى طرف ثالث لا يتحسّن بأن تطرقه ثلاث مرات في ثلاثين ثانية.
لا تُعد ما لا يمكن أن ينجح. فشل تحقق يُعاد خمس مرات هو خمسة إخفاقات متطابقة وتأخير لكل ما خلفه في الطابور. ارمِ شيئًا تعامله المهمة كقاتل، ودعها تفشل من المحاولة الأولى.
الميل الأخير: على أحد أن يراقب
كل إصلاح أعلاه بلا معنى إن كانت الإخفاقات غير مرئية. وأشيع عطل إنتاج نُستدعى
للنظر فيه هو جدول failed_jobs فيه آلاف الصفوف لم يقرأها أحد قط.
ذلك ليس عطل طابور. فشلت المهمة، وسجّل الإطار ذلك تمامًا كما هو موثَّق، ولم يكن شيء موصولًا بالتسجيل. صِل الإخفاقات بما يراقبه فريقك أصلًا، ونبّه على المعدل لا على الحدث، وضع نبض قلب على الطابور نفسه ليتميّز العامل الذي توقف عن طابور فارغ فحسب.
الطابور الذي يمكن تركه ليس طابورًا لا يفشل أبدًا. بل طابور يخبرك حين يفشل، ولا يضرّ حين يكرّر نفسه.
وإن كان طابورك اليوم من النوع الآخر، فذلك هو العمل.
