المشغلات أم عمال الخلفية: أيهما أنسب لإشعارات موثوقة
تعلّم متى تكون المشغلات أو عمال الخلفية أكثر أمانًا للإشعارات، مع إرشادات عملية حول إعادة المحاولة، حدود المعاملات، ومنع التكرارات.

لماذا يفشل تسليم الإشعارات في التطبيقات الحقيقية
الإشعارات تبدو بسيطة: يفعل المستخدم شيئًا ثم يُرسل بريد إلكتروني أو رسالة قصيرة. معظم الأخطاء الحقيقية تتلخّص في التوقيت والتكرار. تُرسل الرسائل قبل أن يُحفظ البيانات فعلاً، أو تُرسل مرتين بعد فشل جزئي.
«الإشعار» يمكن أن يكون أشياء كثيرة: إيصالات بريد إلكتروني، رموز تحقق مرة واحدة عبر SMS، تنبيهات دفع، رسائل داخل التطبيق، تنبيهات Slack أو Telegram، أو webhook إلى نظام آخر. المشكلة المشتركة دائمًا هي نفسها: تحاول مزامنة تغيير في قاعدة البيانات مع شيء خارج تطبيقك.
العالم الخارجي فوضوي. المزودون قد يكونون بطيئين، يرجعون مهلات، أو يقبلون الطلب بينما تطبيقك لم يستلم استجابة النجاح. تطبيقك نفسه قد ينهار أو يعيد التشغيل أثناء الطلب. حتى الإرسالات «الناجحة» قد تُعاد بسبب إعادة المحاولة على البنية التحتية، إعادة تشغيل العمال، أو ضغط المستخدم للزر مرة أخرى.
أسباب شائعة لفشل تسليم الإشعارات تشمل انتهاء مهلة الشبكة، انقطاع أو حدود مزود الخدمة، إعادة تشغيل التطبيق في لحظة خاطئة، محاولات تعيد تنفيذ نفس منطق الإرسال بدون حراسة فريدة، وتصاميم تجعل كتابة قاعدة البيانات والإرسال الخارجي خطوة مجمعة واحدة.
عندما يطلب الناس "إشعارات موثوقة"، فعادةً يقصدون أحد أمرين:
- التسليم مرة واحدة بالضبط، أو
- على الأقل عدم التكرار أبدًا (التكرارات أسوأ غالبًا من التأخير).
الحصول على السرعة والسلامة الكاملة صعب، فتضطر لاختيار مقايضات بين السرعة والسلامة والتعقيد.
لهذا السبب اختيارك بين المشغلات والعمال الخلفيين ليس مجرد نقاش معماري. إنه يتعلق بموعد السماح بالإرسال، كيف تُعاد المحاولات عند الفشل، وكيف تمنع تكرار الرسائل عندما يحدث خطأ.
المشغلات والعمال الخلفيون: ماذا يعني كل منهما
عندما يقارن الناس المشغلات بعمال الخلفية، فإنهم يقارنون في الواقع المكان الذي يعمل فيه منطق الإشعار ومدى ارتباطه بالفعل الذي سبّبه.
المشغل يعني "افعلها الآن عند حدوث X". في كثير من التطبيقات، هذا يعني إرسال بريد أو SMS مباشرة بعد إجراء المستخدم، داخل نفس طلب الويب. يمكن أيضًا أن تكون المشغلات على مستوى قاعدة البيانات: مشغل قاعدة البيانات يعمل تلقائيًا عند إدراج أو تحديث صف. كلا النمطين يبدو فوريًا، لكنه يرث توقيت وحدود ما أطلقه.
العامل الخلفي يعني "افعلها قريبًا، ولكن ليس في الواجهة الأمامية." إنه عملية منفصلة تسحب الوظائف من قائمة انتظار وتحاول تنفيذها. يسجّل تطبيقك الأساسي ما ينبغي فعله ثم يرجع بسرعة، بينما يتعامل العامل مع الأجزاء البطيئة والمعرضة للفشل مثل استدعاء مزود البريد أو الرسائل.
الـ “وظيفة” هي وحدة العمل التي يعالجها العامل. عادةً تتضمن من يجب إخطاره، أي قالب، البيانات المملوءة، الحالة الحالية (queued, processing, sent, failed)، عدد المحاولات، وأحيانًا وقت مجدول.
تدفق إشعار نموذجي يبدو هكذا: تحضّر تفاصيل الرسالة، تضع وظيفة في الطابور، ترسل عبر مزود، تسجل النتيجة، ثم تقرر إعادة المحاولة أو الإيقاف أو تنبيه أحدهم.
حدود المعاملة: متى يكون من الآمن فعلاً الإرسال
حدود المعاملة هي الخط الفاصل بين "حاولنا الحفظ" و"تم الحفظ فعلاً". حتى يتم الالتزام في قاعدة البيانات، يمكن التراجع عن التغيير. هذا مهم لأن الإشعارات يصعب سحبها.
إذا أرسلت بريدًا أو رسالة قصيرة قبل الالتزام، قد تخبر شخصًا بشيء لم يحدث أبدًا. قد يستلم زبون "تم تغيير كلمة المرور" أو "تم تأكيد طلبك" ثم تفشل الكتابة لاحقًا بسبب خطأ قيد أو مهلة. يصبح المستخدم مرتبكًا ويوجب على الدعم فك الخيوط.
الإرسال من داخل مشغل قاعدة البيانات مغري لأنه يعمل تلقائيًا عند تغيير البيانات. المشكلة أن المشغلات تعمل داخل نفس المعاملة. إذا تم التراجع عن المعاملة، قد تكون قد اتصلت بالفعل بمزود البريد أو الرسائل.
المشغلات في قواعد البيانات أيضًا أصعب للرصد والاختبار وإعادة المحاولة بأمان. وعندما تقوم بمكالمات خارجية بطيئة، قد تحتجز أقفالًا لفترة أطول من المتوقع وتجعل تشخيص مشكلات قاعدة البيانات أصعب.
نهج أكثر أمانًا هو فكرة الـ outbox: سجّل نية الإشعار كبيانات، التزم بها، ثم أرسلها بعد الالتزام.
تقوم بعمل التغيير التجاري وتدرج في نفس المعاملة صفًا في outbox يصف الرسالة (من، ماذا، أي قناة، بالإضافة إلى مفتاح فريد). بعد الالتزام، يقرأ عامل خلفي صفوف outbox المعلقة، يرسل الرسالة، ثم يعلّمها كمرسلة.
الإرسالات الفورية قد تكون مقبولة للرسائل غير الحرجة والمعلوماتية التي لا مشكلة من أن تكون خاطئة، مثل "نحن نعالج طلبك." لأي شيء يجب أن يتطابق مع الحالة النهائية، انتظر حتى بعد الالتزام.
إعادة المحاولات والتعامل مع الفشل: أين يفوز كل نهج
إعادة المحاولات عادةً هي الحَكَم.
المشغلات: سريعة، لكنها هشة أمام الأخطاء
معظم التصاميم المعتمدة على المشغلات لا تمتلك قصة إعادة محاولة جيدة.
إذا استدعى المشغل مزود البريد/الرسائل وفشل الاستدعاء، غالبًا ما ينتهي بك الأمر باختيارين سيئين:
- فشل المعاملة (ويُمنع التحديث الأصلي)، أو
- تسجيل الخطأ بصمت (وفقدان الإشعار دون علم).
لا مقبول منهما عندما تكون الموثوقية مهمة.
محاولة حلق أو تأخير داخل المشغل قد تجعل الأمور أسوأ بإبقاء المعاملات مفتوحة لفترة أطول، مما يرفع زمن الأقفال ويبطئ قاعدة البيانات. وإذا ماتت قاعدة البيانات أو التطبيق أثناء الإرسال، غالبًا لا يمكنك معرفة ما إذا كان المزود قد استلم الطلب.
العمال الخلفيون: مصمّمون لإعادة المحاولة
المعاملات الخلفية تعامل الإرسال كمهمة منفصلة لها حالتها الخاصة. هذا يجعل إعادة المحاولة طبيعية.
كقاعدة عملية، عادةً تعيد المحاولة للأخطاء المؤقتة (مهلات، مشكلات شبكة عابرة، أخطاء خادم، حدود معدل مع انتظار أطول). عادةً لا تعيد المحاولة للمشكلات الدائمة (أرقام هاتف غير صالحة، عناوين بريد مكسورة، رفض صريح مثل إلغاء الاشتراك). للأخطاء "غير المعروفة" تحدد حدًا لعدد المحاولات وتعرض الحالة بوضوح.
التراجع الزمني (backoff) يمنع إعادة المحاولات من تفاقم المشكلة. ابدأ بانتظار قصير ثم زيّده في كل مرة (مثلاً 10 ث، 30 ث، 2 د، 10 د)، وتوقّف بعد عدد محدّد من المحاولات.
لجعل هذا يقاوم عمليات النشر وإعادة التشغيل، خزّن حالة إعادة المحاولة مع كل وظيفة: عدد المحاولات، وقت المحاولة التالي، آخر خطأ مختصر ومقروء، آخر وقت محاولة، وحالة واضحة مثل pending, sending, sent, failed.
إذا أعيد تشغيل التطبيق أثناء الإرسال، يمكن لعامل أن يعيد فحص الوظائف العالقة (مثلاً status = sending مع طابع زمني قديم) ويعيد محاولة إجرائها بأمان. هنا تصبح الـ idempotency أساسية حتى لا يسبب تكرار الإرسال إرسالًا مزدوجًا.
منع التكرار في البريد والرسائل بالـ idempotency
الـ idempotency يعني أن بإمكانك تشغيل نفس فعل "إرسال إشعار" أكثر من مرة وسيحصل المستخدم على الرسالة مرة واحدة فقط.
الحالة الكلاسيكية للتكرار هي مهلة: يستدعي تطبيقك مزود البريد/الرسائل، ينتهي الطلب بمهلة، ويعيد تطبيقك المحاولة. قد يكون الطلب الأول قد نجح فعليًا، فإذا أعدت المحاولة تكون النتيجة تكرارًا.
حل عملي هو إعطاء كل رسالة مفتاحًا ثابتًا والتعامل مع هذا المفتاح كمصدر الحقيقة الوحيد. المفاتيح الجيدة تصف معنى الرسالة، لا متى حاولت إرسالها.
نهج شائع يتضمن:
notification_idمولَّد عند قرار "يجب أن توجد هذه الرسالة"، أو- مفتاح مشتق من العمل مثل
order_id + template + recipient(فقط إذا كان ذلك يحدد التفرد فعلاً).
ثم خزّن دفتر إرسال (غالبًا جدول outbox نفسه) واجعل كل محاولات الإرسال تستشير هذا الدفتر قبل الإرسال. اجعل الحالات بسيطة ومرئية: created (مقرر)، queued (جاهز)، sent (مؤكد)، failed (فشل مؤكد)، canceled (لم يعد مطلوبًا). القاعدة الحرجة أن تسمح بسجل نشط واحد فقط لكل مفتاح idempotency.
قد تساعد idempotency على مستوى المزود عندما يدعمها، لكنها لا تغني عن دفترك الخاص. ما زلت بحاجة للتعامل مع إعادة المحاولات، وعمليات النشر، وإعادة تشغيل العمال.
عامل "النتائج المجهولة" باعتبارها حالة من الدرجة الأولى أيضًا. إذا انتهت مهلة الطلب، لا تكرر الإرسال فورًا. علّمها معلّقة للتأكيد وأعد المحاولة بأمان عن طريق التحقق من حالة التسليم لدى المزود عندما يكون ذلك ممكنًا. إذا لم تستطع التأكد، أخر وعلّم بدلاً من الإرسال المزدوج.
نمط افتراضي آمن: outbox + عامل خلفي (خطوة بخطوة)
إن أردت افتراضيًا آمنًا، فإن نمط outbox مع عامل خلفي يصعب منافسته. يبقي الإرسال خارج معاملتك التجارية، مع ضمان حفظ نية الإشعار.
التدفق
عامل "إرسال إشعار" كبيانات تحفظها، لا كفعل تطلقه.
تحفظ التغيير التجاري (مثلاً تبديل حالة الطلب). في نفس المعاملة، تُدرج أيضًا سجلًا في outbox يحتوي المستلم، القناة (email/SMS)، القالب، الحمولة، ومفتاح idempotency. تُكمل المعاملة. فقط بعد تلك النقطة يمكن أن يحدث إرسال.
يختار عامل خلفي صفوف outbox المعلقة بانتظام، يرسلها، ويسجل النتيجة.
أضف خطوة بسيطة للمطالبة حتى لا يلتقط عاملان نفس الصف. قد تكون هذه خطوة تغيير حالة إلى processing أو طابع زمن للقفل.
منع التكرارات والتعامل مع الفشل
التكرارات غالبًا تحدث عندما ينجح الإرسال لكن ينهار تطبيقك قبل أن يسجل "مرسل". تحل هذا بجعل كتابة "معلمة المرسلة" آمنة للتكرار.
استخدم قاعدة تفرد (مثلاً قيد فريد على مفتاح idempotency والقناة). أعِد المحاولة بقواعد واضحة: محاولات محدودة، تأخيرات متزايدة، وفقط للأخطاء القابلة لإعادة المحاولة. بعد آخر محاولة، انقل الوظيفة إلى حالة صندوق الرسائل الميتة (مثل failed_permanent) حتى يراجعها أحدهم ويعيد معالجتها يدويًا إذا لزم.
يمكن أن يبقى المراقبة بسيطة: عدّات للحالات pending، processing، sent، retrying، وfailed_permanent، بالإضافة إلى أقدم طابع زمني معلق.
مثال ملموس: عندما ينتقل طلب من "Packed" إلى "Shipped"، تحدّث صف الطلب وتُنشئ صفًا واحدًا في outbox بمفتاح idempotency order-4815-shipped. حتى لو تعطل العامل أثناء الإرسال، لن يسبب إعادة التشغيل إرسالًا مزدوجًا لأن كتابة "sent" محمية بهذا المفتاح الفريد.
متى تكون العمال الخلفية الخيار الأفضل
المشغلات جيدة للاستجابة فور تغيير البيانات. لكن إذا كانت المهمة "تسليم إشعار بموثوقية في ظروف العالم الواقعي المعقدة"، فالعمال الخلفيون عادةً يمنحونك تحكمًا أكبر.
العمال مناسبون عندما تحتاج إرسالًا زمانيًا (تذكيرات، موجزات)، حجمًا عاليًا مع حدود معدل وضغط خلفي، التسامح مع تقلبات المزود (قيود 429، استجابات بطيئة، انقطاعات قصيرة)، سير عمل متعدد المراحل (أرسل، انتظر التسليم، ثم تابع)، أو أحداث بين أنظمة تحتاج إلى تسوية.
مثال بسيط: تقبض أموالًا من زبونًا ثم ترسل إيصال SMS ثم ترسل فاتورة عبر البريد. إذا فشل SMS بسبب مشكلة بوابة، تريد أن يظل الطلب مدفوعًا وأن تعيد المحاولة لاحقًا بأمان. وضع ذلك في مشغل يخاطر بخلط "صحة البيانات" مع "توفر طرف ثالث الآن".
العمال الخلفيون أيضًا يسهلون التحكم التشغيلي. يمكنك إيقاف طابور أثناء حادثة، فحص الأخطاء، وإعادة المحاولة بتأخيرات.
أخطاء شائعة تسبب رسائل مفقودة أو مكررة
أسرع طريق لعدم موثوقية الإشعارات هو "أرسلها فقط" حيثما كان مريحًا، ثم تأمل أن تعيد المحاولات إنقاذ الموقف. سواء استخدمت مشغلات أو عمالًا، التفاصيل حول الفشل والحالة هي التي تقرر ما إذا كان المستخدم يتلقى رسالة واحدة، اثنتين، أم لا شيء.
فخ شائع هو الإرسال من مشغل قاعدة البيانات والافتراض أنه لا يمكن أن يفشل. المشغلات تعمل داخل المعاملة، لذا أي مكالمة مزود خارجية بطيئة يمكن أن توقف الكتابة، تضرب مهلات، أو تقبض جداولًا أكثر مما توقعت. الأهم، إذا فشل الإرسال وتراجعت المعاملة، قد تعيد لاحقًا وتُرسل مرتين إذا قبل المزود الطلب الأول فعلاً.
أخطاء تتكرر كثيرًا:
- إعادة محاولة كل شيء بنفس الطريقة، بما في ذلك الأخطاء الدائمة (بريد سيء، رقم محظور).
- عدم فصل "queued" عن "sent"، فتفقد القدرة على معرفة ما الآمن لإعادة المحاولة بعد تعطل.
- استخدام الطوابع الزمنية كمفاتيح لتفادي التكرار، فيسمح ذلك بإعادة المحاولة بإنشاء سجلات جديدة.
- إجراء مكالمات المزود في مسار طلب المستخدم (لا تنتظر بوابات أثناء الدفع أو إرسال النموذج).
- معالجة مهلات المزود كـ "غير مُسلَّمة" بينما كثيرًا ما تكون "مجهولة".
مثال بسيط: ترسل SMS، تنتهي مهلة المزود، وتعيد المحاولة. إذا كان الطلب الأول قد نجح فعلاً، يستلم المستخدم رمزين. الإصلاح هو تسجيل مفتاح idempotency ثابت (مثل notification_id)، تعليم الرسالة كـ queued قبل الإرسال، ثم تعليمها كـ sent فقط بعد استجابة نجاح واضحة.
فحوصات سريعة قبل إرسال الإشعارات
معظم أخطاء الإشعارات لا تتعلق بالأداة، بل بالتوقيت، وإعادة المحاولة، والسجلات المفقودة.
تأكد أنك ترسل فقط بعد أن تُصبح كتابة قاعدة البيانات مُلتزمة بأمان. إذا أرسلت داخل نفس المعاملة ثم تراجعت، قد يتلقى المستخدم رسالة عن شيء لم يحدث.
بعد ذلك، امنح كل إشعار هوية فريدة قابلة للتعرّف. اعطِ كل رسالة مفتاح idempotency ثابتًا (مثال order_id + event_type + channel) وفرضه في التخزين حتى لا يُنشئ تكرارات.
قبل الإصدار، افحص الأساسيات:
- الإرسال يحدث بعد الالتزام، لا أثناء الكتابة.
- لكل إشعار مفتاح idempotency فريد، وتُرفض المكررات.
- عمليات إعادة المحاولة آمنة: يمكن تشغيل نفس الوظيفة مجددًا ولن تُرسل أكثر من مرة.
- تُسجّل كل محاولة (الحالة، last_error، الطوابع الزمنية).
- تُحدَّد المحاولات، والعناصر العالقة لها مكان واضح للمراجعة والمعالجة.
اختبر سلوك إعادة التشغيل عمدًا. أوقف العامل أثناء الإرسال وأعد تشغيله وتحقق ألا يحدث إرسال مزدوج. جرّب نفس الشيء أثناء تحميل قاعدة البيانات.
سيناريو بسيط للتحقق: يغيّر المستخدم رقمه، ثم تُرسل رسالة تحقق SMS. إذا انتهت مهلة المزود، يعيد التطبيق المحاولة. مع مفتاح idempotency وسجل المحاولات الجيد، إما ترسل مرة واحدة أو تُعيد المحاولة لاحقًا بأمان، لكن لا تُزعج المستخدم.
سيناريو نموذجي: تحديثات الطلب دون إرسال مزدوج
متجر يرسل نوعين من الرسائل: (1) تأكيد طلب عبر البريد بعد الدفع، و(2) تحديثات SMS عند خروج الحزمة للتوصيل ثم التوصيل النهائي.
ما الذي يخطئ عندما ترسل مبكرًا (مثلاً داخل مشغل قاعدة بيانات)؟ خطوة الدفع تكتب صفًا في orders، يشغل المشغل ويُرسل البريد، ثم يفشل قبض الدفع بعد ثانية. الآن لديك بريد "شكرًا لطلبك" لطلب لم يصبح حقيقيًا.
المشكلة العكسية: حالة التوصيل تتغير إلى "Out for delivery"، تستدعي مزود SMS، وينتهي الطلب بمهلة. لا تعرف إن كانت الرسالة قد أُرسلت. إن كررت فورًا، تخاطر برسالتين. إن لم تُعد المحاولة، قد لا ترسل أبدًا.
التدفق الأكثر أمانًا يستخدم صفًا في outbox وعامل خلفي. التطبيق يُكمل المعاملة لحالة الطلب، وفي نفس المعاملة يكتب صفًا في outbox مثل "أرسل القالب X إلى المستخدم Y، القناة SMS، مفتاح idempotency Z". فقط بعد الالتزام يسلم العامل الرسائل.
جدول زمني بسيط يبدو هكذا:
- ينتهي الدفع بنجاح، تُكمل المعاملة، يُحفَظ صف outbox لتأكيد البريد.
- يرسل العامل البريد ثم يؤشر outbox كمرسل مع معرّف الرسالة من المزود.
- تتغير حالة التوصيل، تُكمل المعاملة، يُحفَظ صف outbox لتحديث SMS.
- ينتهي اتصال المزود بمهلة، يعلّم العامل outbox كقابل لإعادة المحاولة ويحاول لاحقًا بنفس مفتاح idempotency.
عند إعادة المحاولة، صف outbox هو مصدر الحقيقة الوحيد. لا تنشئ "طلب إرسال" ثانيًا، بل تُكمل الأول.
للدعم، هذا أوضح أيضًا. يمكنهم رؤية الرسائل العالقة في "failed" مع آخر خطأ (مهلة، رقم سيئ، بريد محظور)، وعدد المحاولات، وما إذا كان من الآمن إعادة المحاولة دون تكرار.
الخطوات التالية: اختر نمطًا ونفّذه بدقة
اختر افتراضيًا وسجّله. السلوك غير المتسق عادةً يخرج من مزج المشغلات والعمال عشوائيًا.
ابدأ صغيرًا بجدول outbox وحلقة عامل واحدة. الهدف الأول ليس السرعة، بل الصحة: خزّن ما تنوي إرساله، أرسله بعد الالتزام، ولا تعلّمه كمرسل إلا بعد تأكيد المزود.
خطة طرح بسيطة:
- عرّف الأحداث (order_paid, ticket_assigned) والقنوات المسموحة.
- أضف جدول outbox مع event_id، recipient، payload، status، attempts، next_retry_at، sent_at.
- ابنِ عاملًا واحدًا يفحص الصفوف المعلقة، يرسل، ويحدّث الحالة في مكان واحد.
- أضف idempotency بمفتاح فريد لكل رسالة و"لا تفعل شيئًا إذا أُرسلت بالفعل".
- قسّم الأخطاء إلى قابلة لإعادة المحاولة (مهلات، 5xx) مقابل غير قابلة (رقم سيئ، بريد محظور).
قبل أن تزيد الحجم، أضف رؤية أساسية. تتبع عدد المعلّقين، معدل الفشل، وعمر أقدم رسالة معلّقة. إذا استمر عمر الأقدم في الازدياد، فغالبًا لديك عامل معلق، انقطاع مزود، أو خطأ منطقي.
إذا كنت تبني في AppMaster (appmaster.io)، فالنمط يطابق بسهولة: نمذج الـ outbox في Data Designer، اكتب التحديث التجاري وصف outbox في معاملة واحدة، ثم نفّذ منطق الإرسال وإعادة المحاولة في عملية خلفية منفصلة. هذا الفصل هو ما يحافظ على موثوقية تسليم الإشعارات حتى عندما تتصرف المزودات أو عمليات النشر بشكل غير متوقع.
الأسئلة الشائعة
العمال الخلفيون عادةً هم الافتراض الآمن لأن الإرسال بطيء ومعرض للفشل، والعمال مُصممون لإعادة المحاولة وإظهار الحالة. المشغلات قد تكون سريعة، لكنها مرتبطة بإحكام بالمعاملة أو الطلب الذي أطلقها، ما يجعل التعامل مع الفشل والتكرارات أصعب.
هذا خطير لأن عملية كتابة قاعدة البيانات قد تُلغى لاحقًا. قد تُخطر المستخدمين بتغيير كلمة مرور أو طلب لم يتم حفظه فعلاً، ولا يمكنك سحب بريد إلكتروني أو رسالة قصيرة بعد إرسالها.
المشكلة أن المشغل يعمل داخل نفس المعاملة. إذا استدعى مقدم خدمة البريد/الرسائل وبُطىء أو فشل لاحقًا، فقد تكون قد أرسلت رسالة فعلية عن تغيير لم يُطبّق، أو قد تُوقف المعاملة بسبب مكالمة خارجية بطيئة.
نمط outbox يخزن نية الإرسال كسطر في قاعدة البيانات في نفس معاملة تغيير العمل. بعد الالتزام، يقرأ عامل خلفي الصفوف المعلقة، يرسل الرسائل، ويعلّمها كمرسلة، مما يجعل التوقيت وإعادة المحاولة أكثر أمانًا.
في الغالب نتيجة المهلة تكون «مجهولة» وليس «فشلت». النظام الجيد يسجل المحاولة، يؤخر ويعيد المحاولة بأمان باستخدام هوية الرسالة نفسها، بدلًا من الإرسال فورًا مرة أخرى وإحداث تكرار.
استخدم idempotency: امنح كل إشعار مفتاحًا ثابتًا يعبّر عما تعنيه الرسالة (ليس متى حاولت الإرسال). خزّن ذلك في دفتر قيود (غالبًا جدول outbox) واطبّق وجود سجل نشط واحد لكل مفتاح، حتى تكمل إعادة المحاولات نفس الرسالة بدلاً من إنشاء رسالة جديدة.
أعد المحاولة للأخطاء المؤقتة مثل المهلات، استجابات 5xx، أو حدود المعدل (مع انتظار متزايد). لا تُعد المحاولة للأخطاء الدائمة مثل عناوين غير صالحة أو أرقام محظورة أو ارتدادات نهائية؛ علّمها كفشل مرئي ليصححها أحدهم بدلًا من المراسلة المتكررة.
يمكن للعامل الخلفي فحص الوظائف العالقة في حالة sending لفترة أطول من المهلة المعقولة، وإعادتها إلى حالة قابلة لإعادة المحاولة وتجريبها مجددًا بتراجع زمني. هذا يعمل بأمان فقط إذا كانت كل وظيفة تسجل حالة (محاولات، طوابع زمنية، آخر خطأ) والـ idempotency يمنع الإرسال المزدوج.
يعني أن تكون قادرًا على الإجابة عمّا إذا كان «آمنًا إعادة المحاولة؟» خزّن حالات واضحة مثل pending وprocessing وsent وfailed، بالإضافة لعد المحاولات وآخر خطأ. هذا يجعل الدعم وتصحيح الأخطاء عمليًا ويسمح للنظام بالتعافي دون تخمين.
نمذج جدول outbox في Data Designer، اكتب التحديث التجاري وسطر outbox في معاملة واحدة، ثم شغّل منطق الإرسال وإعادة المحاولة في عملية خلفية منفصلة. احتفظ بمفتاح idempotency واحد لكل رسالة ودوّن المحاولات، حتى لا تخلق عمليات النشر وإعادة المحاولة وعمليات إعادة تشغيل العامل تكرارات.


