→ المقالات

التكامل · ASP.NET Core · Odoo

إرسال العملاء المحتملين إلى Odoo دون أن يضيع أحد: صندوق صادر معاملاتي في ASP.NET Core

عميل محتمل يصل إلى الموقع ولا يصل إلى فريق المبيعات أسوأ من ألا يكون هناك موقع أصلًا. سيتوقف نظام إدارة العملاء أحيانًا، والمهم أن ينتظر العميل المحتمل بدل أن يختفي.

6 دقائق قراءة بقلم أنس أمين

كتابتان، وإحداهما ستفشل

أن تحفظ العميل المحتمل ثم تتصل بـOdoo يبدو سليمًا حتى يفشل الاتصال بعد الحفظ: العميل في قاعدة بياناتك، ولا أثر له عند المبيعات. وأن تتصل بـOdoo أولًا يفشل بالعكس، حين ينكسر الحفظ بعد أن صار السجل في النظام. وإعادة المحاولة داخل الطلب لا تفعل إلا أن تُبطئ الصفحة ما دام النظام متوقفًا.

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

// The enquiry and its outbox row commit together, or not at all.
db.Inquiries.Add(inquiry);
db.OutboxMessages.Add(new OutboxMessage
{
    Type = "InquiryCreated",
    AggregateId = inquiry.Id,
    PayloadJson = JsonSerializer.Serialize(InquiryPayload.From(inquiry)),
    OccurredUtc = now,
    Status = OutboxStatus.Pending,
    NextAttemptUtc = now,
});
await db.SaveChangesAsync(ct);
حفظ واحد، ومعاملة SQL واحدة

ماذا تحمل الرسالة

اجعل السجل مملًّا وكاملًا. لا يجب أن يخمّن المُرسِل أبدًا ما هي الرسالة، ولمن هي، ولماذا فشلت آخر مرة.

  • النوع. ما الذي حدث: استفسار جديد، أو مورد اعتُمد، أو بريد يجب إرساله.
  • السجل والحمولة. معرّف السجل الذي تخصه، ولقطة JSON بما يحتاجه الطرف الآخر.
  • الحالة. معلّقة، أو قيد المعالجة، أو مُعالَجة، أو ميتة. لا شيء بينها.
  • المحاولات. كم محاولة حتى الآن، ومتى موعد التالية.
  • عقد الإيجار. أي عامل يمسك الرسالة، وحتى متى.
  • آخر خطأ. رمز وسبب قصير، لا الحمولة ولا تتبع أخطاء مليء ببيانات شخصية.

استلم كل رسالة بعقد إيجار مؤقت

مهمة متكررة في Hangfire تعمل كل 30 ثانية وتمر على الرسائل المستحقة، الأقدم أولًا، لمدة 20 ثانية أو 500 رسالة على الأكثر. وتستلم سجلًا واحدًا في كل مرة بتحديث مشروط: لا ينتقل إلى قيد المعالجة إلا سجل معلّق حان موعده، أو سجل قيد المعالجة انتهى عقده، مع عقد مدته خمس دقائق ومعرّف العامل.

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

var claimed = await db.OutboxMessages
    .Where(m => m.Id == id &&
        ((m.Status == OutboxStatus.Pending && m.NextAttemptUtc <= now) ||
         (m.Status == OutboxStatus.Processing && m.LeaseExpiresUtc < now)))
    .ExecuteUpdateAsync(s => s
        .SetProperty(m => m.Status, OutboxStatus.Processing)
        .SetProperty(m => m.LeaseExpiresUtc, now.AddMinutes(5))
        .SetProperty(m => m.LockedBy, workerId), ct);

if (claimed == 0) continue; // someone else has it
استلم، أو انتقل إلى غيرها

اجعل الكتابة في النظام آمنة إذا تكررت

قد تُسلَّم الرسالة ثم يتعطل العامل قبل أن يسجل ذلك. المحاولة التالية يجب ألا تنشئ عميلًا محتملًا ثانيًا. لذلك يُكتب معرّف GUID الخاص بالسجل في حقل مخصص على العميل في Odoo، وكل تسليم يبحث به أولًا: يحدّث إن وجده، وينشئ إن لم يجده. وجدول للتسليمات بمفتاح فريد لكل مزوّد وعملية وسجل يُبقي صفًّا واحدًا لكل إرسال منطقي.

لكن البحث ثم الإنشاء ما زالا اتصالين. إن جرت رسالتان للسجل نفسه في الوقت نفسه، قد لا تجد أي منهما شيئًا فتنشئ كلتاهما. عالج رسائل السجل الواحد بالترتيب، أو ضع قيدًا فريدًا على ذلك الحقل في Odoo؛ الحقل وحده لا يجعل العملية ذرية.

أعد محاولة ما يمكن أن يُشفى، وأمِت ما لا يُشفى

كل فشل يأتي من عميل Odoo يُصنَّف مرة واحدة في مكان حدوثه، فلا يحتاج المُرسِل إلا إلى قراءة الجواب.

  • أضف تفاوتًا عشوائيًا إلى فترات الانتظار، حتى لا تعيد دفعة من الإخفاقات المحاولة في الدقيقة نفسها.
  • عامِل 408 و429 كأخطاء عابرة، واحترم Retry-After. من السهل تصنيفهما دائمين بالخطأ.
  • اجعل إعادة المحاولة اليدوية تصفّر عدد المحاولات، وإلا نالت الرسالة الميتة محاولة واحدة ثم ماتت من جديد.
أنواع الفشل، وما يفعله المُرسِل
الفشلالنوعما يحدث
انتهاء المهلة، خطأ شبكة، 5xxعابرإعادة بعد دقيقة، ثم 5 و15 و60 و360
401 و403 وأي 4xx آخردائمرسالة ميتة فورًا، ويظهر على العميل أن مزامنته فشلت
حمولة لا يمكن تحويلهادائمرسالة ميتة فورًا
المحاولة الفاشلة السادسةاستسلامرسالة ميتة، وتنبيه للمسؤولين بالمعرّفات فقط

واجهتا Odoo خلف واجهة واحدة

Odoo من 14 إلى 18 يتحدث JSON-RPC بجلسة، وOdoo 19 يضيف واجهة JSON-2 بمفتاح. واجهة واحدة تخفي الاثنتين، ومصنع يختار العميل عند التشغيل من إعداد صريح أو من فحص الإصدار، ويعود إلى JSON-RPC عند الشك. وحين يكون التكامل متوقفًا، يضع عميل لا يفعل شيئًا علامة «لا ينطبق» على الرسائل بدل أن يُفشلها.

اجعل الطابور مرئيًا

  • مقاييس لعدد الرسائل الميتة ولعمر أقدم رسالة معلّقة، مع تنبيه لكل منهما. العمر يكشف مُرسِلًا عالقًا لا يكشفه العدد أبدًا.
  • السجلات تحمل المعرّفات ورموز الأخطاء، لا الأسماء ولا أرقام الهواتف.
  • يستطيع الفريق إعادة مزامنة عميل واحد من صندوق العملاء المحتملين. وإعادة أي رسالة تحتاج المصادقة الثنائية وتُسجَّل في سجل التدقيق.

اختبره أمام شيء يفشل

اختبارات التكامل تشغّل المُرسِل أمام حاوية SQL Server 2022 حقيقية وOdoo مزيف يمكن أن يُطلب منه الفشل. وإن لم تبدأ الحاوية تفشل الاختبارات بدل أن تُتخطّى.

  • الرسالة المسلَّمة تنتهي مُعالَجة، مع عميل محتمل واحد فقط في Odoo؛ وتشغيل المُرسِل مرة أخرى لا يغيّر شيئًا.
  • الخطأ 500 يترك الرسالة معلّقة مع موعد لإعادة المحاولة، والخطأ 401 يجعلها ميتة.
  • العقد المنتهي يُستعاد والعقد الساري يُترك؛ ولا يُرسل بريد مرتين بعد تعطّل.
  • أربعة مُرسِلين يفرّغون عشرين رسالة يرسلون كل واحدة مرة واحدة فقط.
  • اختبار حمل بـk6 وOdoo متوقف يفشل ما لم تنجح 99.5% من النماذج ويبقَ p95 تحت ثانية.

قبل أن تثق به

  • هل يُحفظ السجل مع رسالته الصادرة في معاملة واحدة؟
  • هل يمكن أن يرسل عاملان الرسالة نفسها، وهل يوجد اختبار بعدة عمّال؟
  • هل تعود رسالة العامل المتعطل حين ينتهي عقده؟
  • هل كل كتابة خارجية مرتبطة بمعرّفك أنت وآمنة إذا تكررت؟
  • أي الأخطاء يُعاد، وأيها يموت، ومن يعلم حين تموت رسالة؟
  • هل يستطيع أحد أن يرى عمر أقدم رسالة معلّقة؟
  • الحمولة فيها بيانات شخصية: متى تُحذف؟ قرّر قبل الإطلاق، لا بعده.

مقالات أخرى