الـ Retries و Backoff: ماذا تفعل عندما يفشل الـ API مؤقتاً؟

دقائق القراءة: 6

عندما تبني تكاملاً يعتمد على API خارجي، فأنت لا تتعامل مع منطقك البرمجي فقط، بل مع شبكة، وخوادم، وسياسات حماية، وازدحام لحظي، وأحياناً تحديثات مؤقتة لا تعلم عنها شيئاً. هنا يظهر مفهوم Retries وBackoff كأحد أهم أساليب جعل الأتمتة أكثر استقراراً وواقعية.

الفكرة ببساطة: ليس كل فشل يعني أن الطلب مات نهائياً. كثير من الإخفاقات تكون مؤقتة، مثل انقطاع قصير في الاتصال، أو بطء في الخادم، أو رد 503 بسبب حمل مرتفع، أو حتى رد 429 لأنك تجاوزت الحد المسموح للطلبات. إذا كنت قد قرأت رموز الحالة (HTTP Status Codes): ماذا يخبرك السيرفر بـ 200 أو 404؟ فستعرف أن الكود نفسه غالباً يخبرك إن كان الفشل دائماً أم مؤقتاً.

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

ما المقصود بـ Retries وBackoff؟

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

هذا المفهوم مرتبط مباشرة بأساسيات تصميم الطلبات التي تناولناها في تشريح طلب الـ API: الـ Endpoint، الـ Headers، والـ Body، لأنك لا تعيد الإرسال بشكل أعمى، بل تعيد نفس Request مع فهم كامل لطبيعته، وهل هو آمن للإعادة أم لا.

مثال عملي مبسط

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

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

متى يكون الفشل مؤقتاً ومتى لا يجب إعادة المحاولة؟

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

أمثلة على الأخطاء القابلة لإعادة المحاولة:
408 مهلة طلب، 429 تجاوز المعدل، 500 خطأ داخلي، 502 بوابة معطلة، 503 خدمة غير متاحة، 504 مهلة بوابة، وأخطاء الشبكة مثل انقطاع DNS أو فشل TCP.

أمثلة على الأخطاء غير المناسبة لإعادة المحاولة:
400 لأن البيانات خاطئة، 401 لأن التوثيق فشل، 403 لأن الوصول ممنوع، و404 إذا كان Endpoint أو المورد غير موجود فعلاً.

إذا كان السبب هو انتهاء صلاحية الرمز، فالحل ليس Retry فوري، بل تجديد الاعتماديات أولاً كما شرحنا في التعامل مع الـ Bearer Tokens وتجديد الصلاحيات آلياً.

لماذا لا يكفي الانتظار الثابت؟

الانتظار الثابت مثل 3 ثوانٍ بين كل محاولة قد يعمل في حالات بسيطة، لكنه ليس أفضل اختيار في الأنظمة المزدحمة. إذا فشلت مئات العمليات في نفس اللحظة ثم أعادت المحاولة بعد نفس المدة تماماً، فإنها تصنع موجة ضغط جديدة على الخادم. هذه المشكلة تعرف أحياناً باسم Retry Storm.

لهذا نستخدم غالباً Exponential Backoff، أي أن زمن الانتظار يتضاعف تدريجياً: ثانية، ثم ثانيتان، ثم أربع، ثم ثمانٍ. ويمكن تحسينه أكثر بإضافة Jitter، أي مقدار عشوائي صغير يمنع كل العملاء من إعادة الإرسال في نفس اللحظة.

الأنماط الأكثر استخداماً

  • Fixed Backoff: تأخير ثابت بين المحاولات.
  • Linear Backoff: زيادة تدريجية بسيطة مثل 2 ثم 4 ثم 6 ثوانٍ.
  • Exponential Backoff: تضاعف متدرج وهو الأكثر شيوعاً.
  • Exponential Backoff with Jitter: الأفضل غالباً في البيئات الإنتاجية.

كيف تبني منطق إعادة المحاولة داخل سكربت الأتمتة؟

قبل كتابة الكود، حدد سياسة واضحة: كم محاولة مسموحة؟ ما الأخطاء القابلة لإعادة المحاولة؟ هل الطلب من النوع Idempotent أم قد يؤدي تكراره إلى إنشاء بيانات مكررة؟ هذا مهم جداً خصوصاً مع طلبات POST التي قد تنشئ سجلات جديدة إذا أُرسلت مرتين.

إذا أردت فهماً أعمق لأنواع الطلبات، فارجع إلى شرح أفعال الـ HTTP (GET, POST, PUT, DELETE) والفرق بينها. كما أن موضوع تحديد معدل الطلبات (Rate Limiting): كيف تتجنب الحظر من الخوادم مكمل أساسي هنا، لأن Backoff ليس مجرد تحسين، بل أداة امتثال لسياسات المزود.

async function fetchWithRetry(url, options = {}, maxRetries = 4, baseDelay = 1000) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) {
        return await response.json();
      }

      const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
      const shouldRetry = retryableStatusCodes.includes(response.status);

      if (!shouldRetry || attempt === maxRetries) {
        throw new Error(`Request failed with status ${response.status}`);
      }

      const retryAfter = response.headers.get("Retry-After");
      let delay = retryAfter
        ? Number(retryAfter) * 1000
        : baseDelay * Math.pow(2, attempt);

      const jitter = Math.floor(Math.random() * 300);
      delay += jitter;

      await new Promise(resolve => setTimeout(resolve, delay));
    } catch (error) {
      const isLastAttempt = attempt === maxRetries;

      if (isLastAttempt) {
        throw error;
      }

      const delay = baseDelay * Math.pow(2, attempt) + Math.floor(Math.random() * 300);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

هذا المثال يطبق ثلاث ممارسات ممتازة: التحقق من Status Code، احترام رأس Retry-After إن وُجد، وإضافة Jitter لتخفيف التزامن الجماعي.

توثيق سياسة الفشل داخل فريقك أو منتجك

كثير من مشاكل الأتمتة لا تكون في الكود نفسه، بل في غياب التوثيق. عندما يعمل فريق على سيناريوهات متعددة بين REST API وWebhook وعمليات مجدولة عبر CRON، يجب أن يعرف الجميع متى تتم إعادة المحاولة، ومتى يُرسل تنبيه، ومتى تتوقف العملية نهائياً.

سياسة مقترحة لتوثيق Endpoint حرج:

– الحد الأقصى للمحاولات: 4
– نمط التأخير: Exponential Backoff with Jitter
– الحالات المسموحة للإعادة: 408, 429, 500, 502, 503, 504
– التعامل مع 401: تنفيذ تجديد رمز ثم إعادة طلب واحدة فقط
– التعامل مع الفشل النهائي: تسجيل مفصل في السجلات وإرسال تنبيه إلى البريد أو Telegram

هذا النوع من التوثيق ينسجم مع ما شرحناه سابقاً في توثيق الـ API: كيفية قراءة مستندات Swagger و Redoc، ويمنع القرارات الارتجالية وقت الأعطال.

أفضل الممارسات في بيئات الأتمتة الحقيقية

  • لا تعِد محاولة كل شيء؛ اربط القرار بنوع الخطأ وسياق العملية.
  • استخدم معرفات فريدة أو مفاتيح Idempotency عندما يكون التكرار خطيراً.
  • احترم رؤوس الاستجابة مثل Retry-After بدلاً من فرض توقيتك الخاص دائماً.
  • سجّل رقم المحاولة والسبب والزمن المستغرق حتى تستطيع التحليل لاحقاً.
  • أرسل تنبيهاً بعد الفشل النهائي، ولا تكتفِ بالصمت داخل السجلات.
  • ادمج منطق Retries مع منهجية معالجة الأخطاء (Error Handling): كيف تجعل نظامك “مضاداً للكسر” بدل اعتباره حلاً منفصلاً.

الخلاصة

الفشل المؤقت في عالم API Integration ليس استثناءً، بل جزء طبيعي من التشغيل اليومي. الفرق بين سكربت هاوٍ ونظام إنتاجي محترم هو أن الثاني يتوقع هذا الفشل، ويفهمه، ويستجيب له بسياسة محسوبة لا تضر بالخادم ولا بالمستخدم.

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *