Idempotency في APIs: ليه لازم تعرفها قبل أول deploy في production
المشكلة باختصار
في أي نظام فيه POST endpoint بيعمل side effect (دفع، إرسال بريد، إنشاء طلب)، فيه سيناريو حتمي:
- العميل بعت طلب الدفع.
- السيرفر نفّذ الـ charge فعلاً على Stripe/Paymob.
- الرد (HTTP 200) اتقطع في النت أو timeout قبل ما يوصل للعميل.
- العميل اعتقد إن الطلب فشل، فضغط "ادفع" تاني.
- السيرفر عمل charge تاني. العميل اتسحب منه الفلوس مرتين.
ده مش سيناريو نادر. Stripe نشرت في توثيقها الرسمي إن حوالي 1% إلى 3% من طلبات الدفع بتتعرض لـ retry من طرف العميل. في نظام بـ 100 ألف طلب دفع يومياً، ده معناه آلاف الحالات المحتملة للتكرار.
مثال بسيط قبل التعريف العلمي (للمبتدئين)
تخيل أنت واقف قدام ATM. ضغطت "اسحب 1000 جنيه". الماكينة قعدت تفكر 10 ثواني. شاشتها اتجمّدت. مقتنعت إنها معلّقة فضغطت "اسحب 1000" تاني. السؤال: هل المفروض تطلعلك 1000 ولا 2000؟
الإجابة الصح: 1000. لأن الماكينة لازم تعرف إن الضغطتين دول نفس الطلب، مش طلبين مختلفين. طريقتها في المعرفة دي: كل طلب جواه رقم تسلسلي فريد. لو الماكينة شافت الرقم ده قبل كده، بترجّع نفس النتيجة اللي سجّلتها أول مرة بدون ما تنفذ السحب تاني.
ده بالظبط الفرق بين "الماكينة بتنفذ كل ضغطة" وبين "الماكينة بتنفذ كل طلب فريد مرة واحدة". اللي بيخلّيها فريدة: مفتاح اسمه Idempotency Key.
التعريف العلمي الدقيق
في الرياضيات، العملية f تكون idempotent لو f(f(x)) = f(x). يعني تنفيذها مرة أو N مرة على نفس الدخل يدّي نفس الناتج ونفس الأثر الجانبي.
في HTTP، حسب RFC 9110: GET, PUT, DELETE مفروض يكونوا idempotent بطبيعتهم. POST و PATCH مش idempotent افتراضياً — وده المكان اللي بيحتاج تدخّل يدوي.
المعيار الصناعي للتدخل ده نزل Stripe في 2015 ولسه لحد دلوقتي هو المرجع: العميل يبعت header اسمه Idempotency-Key، قيمته UUID عشوائي يولّده من جنبه. السيرفر بيخزّن أول response لهذا المفتاح، ولو نفس المفتاح جه تاني بيرجّع الـ response المخزّن بدون إعادة تنفيذ. IETF حالياً بتكتب معيار رسمي بنفس الفكرة في draft-ietf-httpapi-idempotency-key-header.