مفتاح مفيد لكنه لا يكفي وحده
في الأنظمة الرقمية التي تتعامل مع المدفوعات والطلبات الحساسة، يُنظر إلى مفتاح Idempotency باعتباره خط الدفاع الذي يمنع تنفيذ العملية نفسها أكثر من مرة. الفكرة تبدو بسيطة: إذا أرسل العميل الطلب نفسه مجدداً، يتعرّف النظام إلى المفتاح ويعيد النتيجة السابقة بدل إعادة تنفيذ العملية.
لكن التجربة العملية تُظهر أن هذا الحل لا يعمل باعتباره ضماناً مطلقاً. المفتاح يخفف كثيراً من التكرار غير المقصود، لكنه يعتمد على سلسلة من الافتراضات التي قد تنهار في لحظة ضغط أو سباق بين طلبين متزامنين أو إعادة استخدام غير صحيحة للمفتاح. وعندما تفشل هذه الافتراضات، يمكن أن يصل النظام إلى نتيجة شديدة الحساسية: نجاح ظاهر في السجل الداخلي، مقابل عملية مالية مكررة لدى جهة خارجية.
هذه الفجوة بين ما يراه النظام وبين ما حدث فعلاً هي ما يجعل تصميم المعاملات في الخدمات الموزعة أكثر تعقيداً من مجرد إضافة حقل عشوائي مع كل طلب.
الوقت المنقضي لا يعني الفشل دائماً
أحد أكثر الأخطاء شيوعاً في الأنظمة الموزعة هو التعامل مع انتهاء المهلة الزمنية على أنه فشل مؤكد. في الواقع، هناك ثلاث حالات محتملة خلف كل مهلة: الطلب لم يصل أصلاً، أو وصل وتم تنفيذه لكن الرد ضاع في الطريق، أو ما زال قيد المعالجة عند الطرف الآخر.
هذه المنطقة الرمادية هي أخطر ما في الموضوع. فإذا قرأ التطبيق المهلة على أنها إخفاق قاطع، فقد يعيد المحاولة، وعندها قد تتحول المحاولة الثانية إلى تنفيذ إضافي كامل، خصوصاً إذا كان الطرف المقابل قد أجرى العملية بالفعل دون أن يتمكن من إعادة الرد في الوقت المناسب.
في أنظمة الدفع مثلاً، هذه الحالة لا تعني مجرد تكرار تقني، بل قد تعني خصماً مالياً مزدوجاً. والأسوأ أن النظام الداخلي قد يسجل محاولة واحدة فقط، بينما الحقيقة الفعلية محفوظة لدى مزود الخدمة الخارجي.
متى يتحول المفتاح إلى سباق
الاعتماد على مفتاح Idempotency يتطلب أولاً أن تكون آلية حجز المفتاح نفسها محصنة ضد السباقات. فإذا كانت الخطوات هي: تحقق من عدم وجود المفتاح، ثم اكتب المفتاح، فإن طلبين قد يمران في اللحظة نفسها قبل أن يلاحظ أي منهما الآخر. عندها يعتقد كل طلب أنه المالك الوحيد، وتبدأ المعالجة مرتين.
الحل الأكثر متانة هو جعل الكتابة نفسها هي خطوة الحجز، عبر قيد فريد في قاعدة البيانات أو آلية مشابهة تمنع أكثر من طلب من الفوز بالمفتاح ذاته. بهذه الطريقة لا يكون السؤال: هل المفتاح متاح؟ بل يصبح: من الذي تمكن من حجزه أولاً؟
هذا الفرق الصغير في الترتيب يغيّر النتيجة جذرياً. فالسباق لا يُحل بالتحقق المتكرر، بل بتصميم يخسر فيه أحد الطلبين تلقائياً وبشكل حتمي.
النية أهم من تطابق المفتاح
المشكلة الثانية لا تتعلق بالتزامن بل بالقصد. قد يعيد العميل استخدام المفتاح نفسه لطلب مختلف في المرة الثانية: مبلغ آخر، منتج آخر، أو بيانات مختلفة. إذا تعامل النظام مع المفتاح وحده باعتباره دليلاً كافياً، فسيرجع إلى الاستجابة القديمة من دون أن يدرك أن المحتوى تغيّر.
لهذا السبب يجب أن يرتبط المفتاح ببصمة للطلب نفسه، وليس بالرقم العشوائي فقط. البصمة ينبغي أن تعكس معنى العملية لا ترتيب الحقول في JSON ولا الطابع الزمني المؤقت أو المعرّفات الخاصة بالتتبع. وإلا فقد يرفض النظام طلباً سليماً لأن نسخة التمثيل النصي تغيّرت، أو يقبل تطابقاً زائفاً لأن الحقول المهمة لم تُدرج في التحقق.
بمعنى آخر، المفتاح يقول: هذه محاولة متكررة. أما البصمة فتقول: هذه بالفعل العملية نفسها.
ما الذي يجوز للنظام أن يتذكره
ليس كل ما يُخزّن يجب أن يُعاد تشغيله. هنا تظهر مشكلة الذاكرة. إذا احتفظ النظام بكل أنواع الاستجابات المرتبطة بالمفتاح، بما فيها أخطاء الرفض المؤقت أو فشل التحقق أو الحالات التي يمكن أن تتغير بعد قليل، فقد يحبس المستخدم داخل نتيجة قديمة لا تعكس الواقع الجديد.
في المقابل، بعض الأخطاء ليست مؤقتة بل نهائية، مثل الرفض المرتبط ببطاقة مسروقة أو حالة لا يفيد معها الإعادة. أما حالات مثل نقص الرصيد أو أخطاء التحقق القابلة للتغير، فيجب أن تفتح الباب أمام محاولة جديدة بعد تصحيح السبب.
القاعدة العملية هنا هي أن النظام لا ينبغي أن يحفظ كل شيء، بل فقط ما يمكن إعادة استخدامه بأمان. وكلما كانت الاستجابة مرتبطة بحالة خارجية متغيرة، زادت الحاجة إلى الحذر قبل تخزينها على أنها حقيقة نهائية.
حدود الضمان تبدأ خارج النظام
حتى بعد ضبط الحجز والبصمة والذاكرة، يبقى هناك حد واضح: ما الذي يحدث عندما تكون الخدمة الخارجية نفسها بلا دعم حقيقي لضمانات التكرار؟ هنا يصل مفتاح Idempotency إلى نهايته الطبيعية. فهو يقي داخل حدود النظام الذي يسيطر عليه المطوّر، لكنه لا يستطيع تغيير سلوك مزود لا يحتفظ بالمفتاح أو لا يطبق منطقاً مماثلاً.
عند هذه النقطة، يصبح الحل أقرب إلى الهندسة التشغيلية منه إلى مجرد منطق برمجي: تسجيل حالة مؤقتة قبل التنفيذ، التحقق من الوضع قبل إعادة المحاولة، والمقارنة بين السجلات الداخلية والخارجية لاحقاً لاكتشاف أي عملية تسللت دون تطابق.
وفي حال تعطل مخزن المفاتيح نفسه، يبرز سؤال تشغيلي بالغ الأهمية: هل نواصل قبول الطلبات بلا حماية، أم نوقف المعالجة مؤقتاً؟ في المدفوعات، الخيار الأكثر أماناً غالباً هو التوقف، لأن خسارة عملية بيع واحدة أقل كلفة بكثير من خصم مالي مكرر ينهك الدعم والمحاسبة والثقة معاً.
الاختبار الحقيقي في التصميم وليس بعد الحادثة
الدرس الأهم هنا أن نجاح Idempotency لا ينبغي قياسه عند عرض السجل الداخلي فقط، بل عند إعادة تشغيل الطلب في ظروف مختلفة: مرة بعد فشل، ومرة بالتوازي، ومرة مع اختلاف طفيف في المحتوى. إذا غيّر التنفيذ الثاني النتيجة، فالتصميم لم ينجح بعد.
لذلك يوصي المهندسون بطرح ثلاث أسئلة في أي مراجعة تصميم لعملية تغيّر البيانات:
- ماذا يحدث إذا نُفذت العملية مرتين؟
- هل يمكن إثبات أن المحاولة الثانية لا تغيّر شيئاً؟
- أين توجد الحقيقة عندما يختلف النظام الداخلي مع الطرف الخارجي؟
هذه الأسئلة تبدو بسيطة، لكنها تكشف بسرعة إن كان الحل مجرد مفتاح إضافي في قاعدة البيانات، أم تصميم متكامل يواجه المهلة، والسباق، والذاكرة، وحدود السيطرة.
الخلاصة أن مفتاح Idempotency أداة مهمة، لكنه ليس تعهداً سحرياً بالتنفيذ مرة واحدة. الضمان الحقيقي يأتي من مجموعة طبقات تعمل معاً: حجز لا يتعرض للسباق، وبصمة تعكس نية الطلب، وذاكرة لا تخزن إلا ما يمكن استعادته بأمان، وحدود واضحة لما يملكه النظام وما لا يملكه. عندها فقط يقترب النظام من السلوك المتوقع في بيئة لا تمنح أحداً يقيناً كاملاً.