كانت إعادة المحاولة تدمر بياناتنا: كيف أنقذتنا ‘اللامتناهية’ (Idempotency) من جحيم العمليات المكررة؟

يا جماعة الخير، السلام عليكم ورحمة الله.

اسمحولي أبدأ بقصة صارت معي قبل كم سنة، قصة من القصص اللي بتعلّم في الواحد وما بنساها. كانت الساعة حوالي 2 بعد نص الليل، وأنا في سابع نومة. فجأة، التلفون برن، وصوت الإنذارات من نظام المراقبة (Monitoring System) زي فرقة حسب الله اللي صحت فجأة في نص الدار. فتحت اللابتوب وعيوني لسا مش شايفة الشاشة منيح، ولقيت الكارثة.

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

بعد ساعات من التحقيق والقهوة السادة، اكتشفنا السبب. كان في مشكلة بسيطة في الشبكة بين واحد من تطبيقات الموبايل والسيرفرات تبعتنا. التطبيق، بكل براءة، كان لما ما يوصله رد من السيرفر بسرعة، بعيد إرسال طلب الدفع مرة ثانية (Retry). والسيرفر تبعنا، بكل سذاجة، كان يعامل كل طلب جديد كأنه عملية دفع جديدة تمامًا. تخيلوا معي، كل “إعادة محاولة” كانت بتسحب مصاري حقيقية من حساب العميل. كانت ليلة من الجحيم، وما بنساها بحياتي.

هذيك الليلة، تعلمت درس قاسي عن مبدأ برمجي اسمه اللامتناهية (Idempotency). ومن يومها، صرت أعتبره حجر أساس في أي نظام ببنيه، خصوصًا لو فيه عمليات حساسة زي الدفعات أو تعديل البيانات. اليوم، بدي أحكيلكم عن هالمبدأ وكيف ممكن ينقذكم من نفس الكابوس.

ما هي ‘اللامتناهية’ (Idempotency)؟ تبسيط المفهوم المعقّد

ببساطة شديدة، العملية اللامتناهية (Idempotent Operation) هي أي عملية لو نفذتها مرة واحدة أو نفذتها ألف مرة، النتيجة النهائية على النظام بتكون نفسها. ما بتتغير.

خلونا ناخذ مثال من الحياة اليومية عشان الصورة توضح:

  • مثال لامتناهي (Idempotent): زر طلب المصعد. لما تضغط على الزر، المصعد بتلقى الطلب وبيبدأ يتحرك ناحيتك. لو رجعت ضغطت على نفس الزر عشر مرات زيادة، هل رح يوصلك عشر مصاعد؟ لأ طبعًا. النتيجة النهائية وحدة: المصعد قيد الاستدعاء. حالة النظام (المصعد) ما بتتغير بعد الضغطة الأولى.
  • مثال غير لامتناهي (Non-Idempotent): سحب الفلوس من الصراف الآلي. لو سحبت 100 دينار، وبعدها رجعت سحبت كمان 100 دينار، النتيجة النهائية مختلفة تمامًا. رصيدك نقص 200 دينار، مش 100. كل عملية أثرت على حالة النظام (حسابك البنكي) بشكل مستقل.

في عالم البرمجة، عملية “إنشاء فاتورة جديدة” هي عملية غير لامتناهية بطبيعتها. كل مرة بتستدعيها، بتنشئ فاتورة جديدة. وهذا هو بالضبط اللي سبب لنا الكارثة.

ليش الموضوع هاد “كريتيكال” (Critical) في تصميم الأنظمة الحديثة؟

يمكن زمان، لما كانت الأنظمة كلها قطعة واحدة (Monolith) والتواصل بصير جوا نفس الجهاز، كانت المشكلة أقل. بس اليوم، في عالم الخدمات المصغرة (Microservices) والـ APIs اللي بتربط كل إشي ببعضه، اللامتناهية ما عادت رفاهية، صارت ضرورة قصوى. والسبب:

  1. الشبكات غير موثوقة: القاعدة الأولى في الحوسبة الموزعة هي “لا تثق بالشبكة أبدًا”. ممكن الاتصال يقطع، ممكن يصير تأخير، ممكن السيرفر يرد بس الرد ما يوصل للعميل. في كل هاي الحالات، العميل (سواء كان تطبيق موبايل، فرونت-إند، أو سيرفر ثاني) رح يحاول يعيد الطلب.
  2. منطق إعادة المحاولة (Retry Logic): إعادة المحاولة هي نمط تصميمي أساسي للتعامل مع الأخطاء المؤقتة في الشبكة. بس إذا كانت العملية المستدعاة مش لامتناهية، فكل إعادة محاولة هي قنبلة موقوتة.
  3. أنظمة قائمة على الأحداث (Event-Driven Systems): في أنظمة مثل Kafka أو RabbitMQ، في ضمان اسمه “at-least-once delivery”. معناه إنه النظام بضمنلك إن الرسالة رح توصل للمستهلك (Consumer) مرة واحدة على الأقل. “على الأقل” هاي بتعني إنها ممكن توصل مرتين! إذا معالجة الرسالة مش لامتناهية، فإنت في ورطة.

باختصار، أي عملية حساسة (تغيير بيانات، عمليات مالية) يتم استدعاؤها عبر الشبكة يجب، وأكرر، يجب أن تُصمَّم لتكون لامتناهية.

كيف نطبّق اللامتناهية عمليًا؟ (The How-To)

الحكي النظري حلو، بس كيف منعملها على أرض الواقع؟ الحل السحري يكمن في إشي اسمه “مفتاح اللامتناهية” (Idempotency Key).

مفتاح اللامتناهية (The Idempotency Key)

الفكرة بسيطة جدًا وعبقرية بنفس الوقت:

  1. العميل (Client)، قبل ما يرسل الطلب الحساس (مثل إنشاء دفعة)، يقوم بإنشاء معرّف فريد لهذه العملية بالذات. عادةً بكون UUID (Universally Unique Identifier). هذا المعرّف بنسميه “مفتاح اللامتناهية”.
  2. العميل يرسل هذا المفتاح مع الطلب، غالبًا في الـ Header. مثلًا: Idempotency-Key: a1b2c3d4-e5f6-7890-1234-567890abcdef
  3. السيرفر (Server)، لما يستقبل الطلب، بيعمل الآتي:
    • أولًا: بتفقد وجود الـ `Idempotency-Key` في الـ Header.
    • ثانيًا: بيبحث عن هذا المفتاح في مكان تخزين مؤقت (مثل Redis أو جدول في قاعدة البيانات).
    • الحالة (أ) – المفتاح غير موجود: هذا طلب جديد. السيرفر بينفذ العملية كالمعتاد (مثلًا، بخصم المبلغ)، وقبل ما يرجع الرد للعميل، بيخزن نتيجة العملية (الرد كاملًا مع status code) مع مفتاح اللامتناهية.
    • الحالة (ب) – المفتاح موجود: هذا يعني أن العملية إما تم تنفيذها سابقًا أو قيد التنفيذ. السيرفر ما برجع بنفذ العملية مرة ثانية! بدلًا من ذلك، بجيب النتيجة المخزنة مسبقًا من المرة الأولى وبرجعها للعميل كما هي.

بهذه الطريقة، حتى لو العميل أرسل نفس الطلب مع نفس المفتاح ألف مرة، العملية الحساسة (خصم المبلغ) رح تتنفذ مرة واحدة فقط. وباقي الـ 999 مرة، السيرفر رح يرجعله نفس الرد الأصلي بدون ما يعمل أي إشي.

مثال كود (Node.js و Express)

عشان الصورة تكون أوضح، هي مثال مبسط جدًا على شكل Middleware في Express.js. تخيلوا عنا قاعدة بيانات وهمية لتخزين المفاتيح (في الواقع لازم نستخدم Redis أو شي مشابه).


const express = require('express');
const { v4: uuidv4 } = require('uuid'); // لتوليد المفاتيح

const app = express();
app.use(express.json());

// في نظام حقيقي، استخدم Redis أو قاعدة بيانات!
// هذا فقط للتوضيح.
const idempotencyStore = new Map();

// Middleware للتعامل مع اللامتناهية
async function idempotencyMiddleware(req, res, next) {
    const idempotencyKey = req.headers['idempotency-key'];

    if (!idempotencyKey) {
        // إذا كان المفتاح إلزاميًا، يمكن إرجاع خطأ هنا
        return next(); 
    }

    // 1. هل تم تنفيذ هذا الطلب من قبل؟
    if (idempotencyStore.has(idempotencyKey)) {
        console.log(`[${idempotencyKey}] طلب مكرر، إعادة الرد المخزن.`);
        const cachedResponse = idempotencyStore.get(idempotencyKey);
        return res.status(cachedResponse.statusCode).json(cachedResponse.body);
    }

    // نحفظ الرد الأصلي لنتمكن من تخزينه لاحقًا
    const originalJson = res.json;
    const originalStatus = res.status;

    let responseBody;
    let responseStatusCode;

    res.json = (body) => {
        responseBody = body;
        return originalJson.call(res, body);
    };

    res.status = (statusCode) => {
        responseStatusCode = statusCode;
        return originalStatus.call(res, statusCode);
    };

    // 2. عند انتهاء الطلب، نخزن النتيجة
    res.on('finish', () => {
        if (responseStatusCode >= 200 && responseStatusCode  {
                idempotencyStore.delete(idempotencyKey);
            }, 24 * 60 * 60 * 1000); // حذف بعد 24 ساعة
        }
    });

    next();
}

app.use(idempotencyMiddleware);

// Endpoint لإنشاء دفعة
app.post('/api/payments', async (req, res) => {
    const { amount, currency } = req.body;

    try {
        // --- هنا يتم تنفيذ منطق العمل الحساس ---
        console.log(`تنفيذ عملية دفع جديدة بمبلغ ${amount} ${currency}...`);
        // await processPaymentInDatabase(amount, currency);
        // --- انتهى منطق العمل ---

        const paymentId = uuidv4();
        res.status(201).json({ 
            status: 'success', 
            message: 'Payment processed successfully.',
            paymentId: paymentId 
        });
    } catch (error) {
        res.status(500).json({ status: 'error', message: 'Payment failed.' });
    }
});

app.listen(3000, () => console.log('Server running on port 3000'));

نصائح من مطبخ أبو عمر 🧔

على مدار السنين، تعلمت كم شغلة مهمة عن تطبيق اللامتناهية. خذوا هالنصايح من أخوكم:

  • لا تخترع العجلة: قبل ما تكتب سطر كود، شوف الخدمات اللي بتستخدمها. بوابات دفع عالمية زي Stripe بتعتبر الـ Idempotency-Key جزء أساسي من تصميم الـ API تبعها. اقرأ التوثيق تبعهم واتعلم من أفضل الممارسات.
  • عمر المفتاح مهم (Key Expiration): لا تخزن مفاتيح اللامتناهية للأبد. هاد إهدار للمساحة. حدد فترة زمنية معقولة (TTL – Time To Live)، مثل 24 ساعة. ما في داعي أبدًا إنك تتعامل مع إعادة محاولة لطلب من أسبوع فات.
  • العميل هو المسؤول عن تفرد المفتاح: لازم توضح في توثيق الـ API تبعك إن العميل هو المسؤول عن إنشاء مفتاح فريد *لكل عملية* بده ينفذها. لو العميل استخدم نفس المفتاح لعمليتين مختلفتين (مثلًا، دفعتين مختلفتين)، العملية الثانية رح تفشل ظلمًا. استخدام UUIDs هو الحل الأمثل.
  • وين تخزن المفاتيح؟: المثال اللي فوق استخدم `Map` في الذاكرة، وهذا بينفع بس للشرح. في بيئة الإنتاج (Production)، إنت بحاجة لمخزن سريع ومستقل زي Redis. هو مثالي لهيك مهمة لأنه سريع جدًا وبدعم الـ TTL بشكل مباشر.
  • احذر من حالات السباق (Race Conditions): تخيل لو وصلك طلبين بنفس المفتاح في نفس اللحظة بالضبط على سيرفرين مختلفين. الاثنين رح يشوفوا إن المفتاح مش موجود ويبدأوا بتنفيذ العملية! الحل هنا هو استخدام قفل موزّع (Distributed Lock). لما تشوف مفتاح لأول مرة، بتحاول “تقفله” في Redis. اللي بنجح في القفل هو اللي بكمل، والثاني بنتظر أو برجع خطأ.

الخلاصة: اللامتناهية مش رفاهية، هي ضرورة 🙏

يا جماعة، العالم الرقمي اليوم مبني على أنظمة موزعة بتتكلم مع بعضها عبر شبكات غير مضمونة. في هذا العالم، تجاهل مبدأ اللامتناهية هو وصفة أكيدة لكارثة قادمة، سواء كانت بيانات فاسدة، أو خسائر مالية، أو فقدان ثقة العملاء، وهو الأهم.

تطبيق اللامتناهية باستخدام `Idempotency-Key` هو نمط تصميمي قوي، بيحول عمليات إعادة المحاولة الخطيرة إلى عمليات آمنة ومضمونة. هو الدرع اللي بيحميك من كوابيس منتصف الليل ومن مكالمات الدعم الفني الغاضبة.

نصيحتي الأخيرة: راجعوا الـ Endpoints الحساسة في أنظمتكم اليوم. اسألوا نفسكم: “ماذا لو تم استدعاء هذا الـ API مرتين بالخطأ؟”. إذا كانت الإجابة “ستحدث كارثة”، فأنتم تعرفون ما يجب عليكم فعله. ابدأوا بتطبيقها اليوم قبل ما تصير المصيبة.

الله يوفقكم ويبعد عنكم الكوارث البرمجية!

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

أتمتة العمليات

كان مطورنا الجديد ينتظر أياماً: كيف أنقذتنا ‘أتمتة إعداد البيئة’ من جحيم الأسبوع الأول الضائع؟

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

15 مايو، 2026 قراءة المزيد
​معمارية البرمجيات

كانت خدماتنا تتحدث في نفس الوقت: كيف أنقذتنا ‘المعمارية القائِمَة على الأحداث’ (EDA) من جحيم الاقتران المحكم؟

في ليلة إطلاق عصيبة، كادت خدماتنا المترابطة أن تُغرق المشروع بأكمله. أروي لكم كيف تحولنا من فوضى الاقتران المحكم إلى مرونة المعمارية القائمة على الأحداث...

15 مايو، 2026 قراءة المزيد
ذكاء اصطناعي

كانت نماذجنا تموت بصمت: كيف أنقذتنا ‘مراقبة تعلم الآلة’ (ML Monitoring) من كارثة التنبؤات الفاسدة؟

أشارككم قصة حقيقية من الميدان، حين كادت نماذج الذكاء الاصطناعي التي بنيناها بجهد أن تنهار بصمت. اكتشفوا معنا ما هي "مراقبة تعلم الآلة" (ML Monitoring)،...

15 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

نقرات المستخدمين كانت تذهب في الفراغ: كيف أنقذتنا ‘التفاعلات الدقيقة’ من جحيم التجربة الصامتة؟

أشارككم قصة حقيقية من قلب معركة تطوير أحد التطبيقات، وكيف أن تفاصيل صغيرة تُدعى "التفاعلات الدقيقة" (Microinteractions) حوّلت تجربة مستخدم صامتة ومحبطة إلى حوار ممتع...

15 مايو، 2026 قراءة المزيد
برمجة وقواعد بيانات

كانت استعلاماتنا تزحف كالسلحفاة: كيف أنقذنا ‘فهرس قاعدة البيانات’ من جحيم البحث الكامل في الجداول (Full Table Scan)؟

أشارككم قصة حقيقية عن يوم كادت فيه استعلاماتنا البطيئة أن تدمر مشروعًا بالكامل، وكيف كان "الفهرس" البسيط في قاعدة البيانات هو طوق النجاة. سنتعلم معًا...

15 مايو، 2026 قراءة المزيد
الشبكات والـ APIs

كانت خوادمنا تستجدي التحديثات: كيف أنقذتنا ‘خطافات الويب’ (Webhooks) من جحيم الاستطلاع المستمر (Polling)؟

بصفتي أبو عمر، أشارككم قصة حقيقية من معاناتنا مع استنزاف الموارد بسبب الاستطلاع المستمر (Polling). سأشرح كيف كانت خطافات الويب (Webhooks) هي طوق النجاة، مع...

15 مايو، 2026 قراءة المزيد
الحوسبة السحابية

كانت بيئاتنا تتغير كالحرباء: كيف أنقذتنا ‘البنية التحتية كشيفرة’ (IaC) من جحيم التكوين اليدوي؟

أروي لكم حكايتي كـ "أبو عمر"، مبرمج فلسطيني، مع الفوضى التي كنا نعيشها في إدارة الخوادم يدوياً. سأشارككم كيف كانت 'البنية التحتية كشيفرة' (IaC) وأداة...

15 مايو، 2026 قراءة المزيد
البودكاست