الدليل

منع تكرار العمليات

كل ما يحرّك مالًا أو يجهّز خدمة يأخذ request_id من العميل. أعد استعمال المفتاح نفسه في كل إعادة محاولة لنيّة واحدة، وإلا دفعت مرتين.

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

ويجيب X-Radius عن ذلك بالطريقة الوحيدة التي تنجح فعلًا: أنت تسمّي النيّة، والخادم يضمن أن النيّة المسمّاة تقع مرةً واحدة على الأكثر.

القاعدة الوحيدة

أعد استعمال request_id نفسه في كل إعادة محاولة للنيّة نفسها.

ولّد مُعرّف UUID واحدًا حين تقرّر تنفيذ العملية، لا حين ترسل طلب HTTP. واحتفظ به طوال عمر تلك النيّة — عبر انقضاء المهل، وعبر إعادة الاتصال، وعبر إعادة تشغيل العملية إن كان طابورك ينجو منها. فإعادة المحاولة بمفتاح جديد ليست إعادة محاولة، بل هي تعليمة ثانية مستقلة بخصم المحفظة، وسينفّذها الخادم، لأن ذلك بالضبط ما طلبته.

REQ=$(uuidgen)

# المحاولة الأولى. ينقطع الاتصال، ولا ترى الرد أبدًا.
curl -sS -X POST https://acme.example.com/api/v1/users/4711/deposit \
  -H "Authorization: Bearer $XRADIUS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"amount\": 250.00, \"request_id\": \"$REQ\"}"

# إعادة المحاولة. المفتاح نفسه. يُقيَّد للمشترك مرةً واحدة إجمالًا.
curl -sS -X POST https://acme.example.com/api/v1/users/4711/deposit \
  -H "Authorization: Bearer $XRADIUS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"amount\": 250.00, \"request_id\": \"$REQ\"}"

أين يقع الضمان فعلًا

ليس وسيطًا برمجيًا وليس ذاكرة وسيطة. إنه قيد في قاعدة البيانات، موضوع يدويًا في كل مجال على الجدول الذي يسجّل وقوع الشيء:

  • card_redemptions عليه UNIQUE (tenant_id, request_id) — وهو الصف الذي يقول إن كرتًا أُنفق.
  • user_activations عليه UNIQUE (tenant_id, request_id) — وهو الصف الذي يقول إن خطةً جُهِّزت.
  • manager_journal عليه UNIQUE (tenant_id, request_id) — وهو سطر الأستاذ الذي يقول إن مالًا تحرّك.

وهذا الموضع هو بيت القصيد. فالتفرّد على سجلّ الأثر نفسه، وداخل المعاملة نفسها التي أحدثت الأثر، فلا يستطيع "خُصم سلفًا" و"سُجّل الخصم" أن يختلفا. أما ذاكرة وسيطة أمام المعالج فتكون رأيًا ثانيًا، والآراء الثانية تنحرف.

والعمليات التي تتركّب من آثار عدة تشتقّ مفاتيح فرعية من مفتاحك بفاصل : — فتفعيلٌ يُدفع بكرت يكتب ساق المحفظة وساق الكرت وصف التجهيز بمفاتيح مشتقّة من المفتاح الواحد الذي أرسلته، فتكون كل خطوة آمنة ضد التكرار وحدها ويبقى الكل نيّةً واحدة.

ولهذا أيضًا لا يجوز أن يحتوي مفتاحك على : ولا |، ولا أن يبدأ بإحدى البادئات الداخلية المحجوزة. والمفتاح المخالف يُرفض بـ 400 وdetails.reason تساوي request_id_reserved. والمفاتيح محدودة بـ 255 محرفًا (request_id_too_long)، وغيابه عن نقطة تشترطه هو request_id_required. ومعرّف UUID من الإصدار الرابع يحقّق الثلاثة بلا تفكير، ولهذا يولّده كل عميل شحنّاه.

أي العمليات تشترطه

كل نقطة تحرّك مالًا أو تستهلك مخزونًا أو تجهّز خدمة تأخذ request_id إلزاميًا في الجسم:

المجال النقاط
محفظة المشترك POST /users/{id}/deposit و/withdraw و/pay-debt
التفعيل POST /users/{id}/activation
الإضافات والنقاط POST /users/{id}/addons و/redeem-points
الاسترجاع POST /users/{id}/refund-activation وPOST /admin/cards/redemptions/{id}/reversals
الكروت POST /cards/redeem و/admin/cards/redeem-otc و/cards/redeem-create و/admin/cards/redeem-to-wallet
مخزون الكروت POST /card-batches و/card-batches/{id}/regenerate ونقاط النقل الثلاث و/card-templates/{id}/generate
محفظة المدير POST /managers/{id}/wallet/deposit و/withdraw و/pay-debt و/topup
نقطة البيع POST /pos/sales
المدفوعات نقاط بدء البوابة، وهي تشترط UUID تحديدًا

ومجموعة ثانية تقبله وتولّد UUID في الخادم حين تحذفه — مهام التصدير والنسخ الاحتياطي. وذلك آمن لها وعديم الفائدة لك: فالمفتاح المولَّد في الخادم فريد لكل طلب لا لكل نيّة، فلا يمنع تكرارًا عبر إعادات محاولتك. فأرسل مفتاحك أنت إن كان الأمر يعنيك.

وrequest_id ليس أبدًا معامل استعلام ولا ترويسة في هذه الواجهة. هو دائمًا حقل في جسم JSON.

ماذا يعيد التكرار

التكرار نجاح لا خطأ. تحصل على 200 وعلى النتيجة الأصلية، مقروءةً من التخزين لا محسوبةً من جديد. لا يُخصم شيء، ولا يُجهَّز شيء، ولا تُمنح حصة مرتين.

أما كيف يقول الردّ إنه تكرار فيختلف بحسب المجال، وهذا هو الجزء الجدير بالمراجعة على النقطة التي تناديها بعينها:

  • المال والتفعيل يحملان قيمة منطقية صريحة. فـPOST /users/{id}/activation يعيد "replay": true، ومبدّلات المحفظة تعيدها كذلك. والآثار الجانبية غير الآمنة ضد التكرار بذاتها — إصدار مستند فاتورة مثلًا — تُتخطّى لأن الخادم فحص تلك الراية.
  • توليد سلسلة الكروت يشير إليه بحالة HTTP كما بالجسم: فـPOST /card-batches الجديد هو 202 Accepted، وتكراره 200 OK، والجسم يحمل "replay": true مع batch_id الأصلي.
  • أما استبدال الكرت فلا يشير إليه إطلاقًا. فـPOST /cards/redeem يعيد 200 ومعه card_id وmode ولقطة effect_applied نفسها التي أعادها أول مرة، بلا أي راية تكرار. وهناك فرق ظاهر واحد يقرأ كعطل إن لم تكن تتوقعه: الحقل new_balance غائب في التكرار. فالرد الأول يحمل الرصيد بعد التقييد، أما المكرَّر فيحذف الحقل، لأن مسار التكرار يعيد الأثر المخزَّن بلا قراءة المحفظة من جديد. فلا تعدّ غياب new_balance استبدالًا فاشلًا، ولا تعد المحاولة بسببه — اقرأ الرصيد من سجل المشترك إن احتجته.

حين يُرفض المفتاح

الـ 409 ERR_CONFLICT مع details.reason تساوي request_id_conflict يعني أن المفتاح مرتبط سلفًا بموضوع مختلف — كرت آخر، أو مشترك آخر، أو سطر أستاذ لمدير آخر. وهو ليس تكرارًا لطلبك ولا شيئًا يستطيع الخادم الكتابة فوقه.

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

وللتفعيل نسخته الخاصة: activation_request_conflict، حين يكون المفتاح مرتبطًا سلفًا بتفعيل مشترك مختلف. فمفاتيح منع التكرار غير قابلة للنقل بين المواضيع.

نمط يعمل

  1. قرّر تنفيذ العملية. ولّد UUID واحفظه مع سجلّك أنت للنيّة، قبل أول نداء HTTP.
  2. أرسل. وعند 2xx سجّل النتيجة وانتهيت.
  3. وعند انقضاء مهلة أو انقطاع اتصال أو 5xx أو ERR_TIMEOUT، أعد إرسال البايتات نفسها بـrequest_id نفسه. وكرّر بتراجع تدريجي.
  4. وعند ERR_VALIDATION أو ERR_CONFLICT، توقّف. فالطلب خاطئ لا سيّئ الحظ، والبايتات نفسها ستفشل بالطريقة نفسها.
  5. وإن كنت فعلًا لا تعرف هل غادرت العملية عمليتك أصلًا، فأرسلها. فذلك ما وُجد المفتاح من أجله.

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

آخر تحديث

اطرح سؤالًا

جرّبه على شبكتك.

50 مشترك لمدة 7 أيام، من غير ما تدفع حاجة.