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


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

يا هلا بالجميع! كثير مننا يشتغل مع APIs، ودايم تصير مشاكل مؤقتة: الشبكة تفصل، السيرفر مشغول، إلخ. وش تسوي؟ مو معقول تخلي تطبيقك ينهار. هنا يجي دور الـ Retries والـ Backoff.

ليش نحتاج Retries أصلاً؟

تخيل قاعد تتصل بـ API مهم. فجأة، يرجع لك خطأ 500 أو 503. هل هذا يعني إن الـ API خربان للأبد؟ غالباً لا. ممكن يكون ضغط مؤقت، صيانة بسيطة، أو حتى مشكلة شبكة عابرة. الـ Retries ببساطة إنك تحاول مرة ثانية وثالثة... إلخ.

أبسط طريقة: الـ Retry المباشر

يعني تحاول مرة ثانية مباشرة بعد الفشل.

``<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">python
import requests
import time

def call_api_simple_retry(url, max_retries=3):
    for i in range(max_retries):
        try:
            response = requests.get(url)
            response.raise_for_status() # Raise an exception for HTTP errors
            return response.json()
        except requests.exceptions.RequestException as e:
            print(f"Attempt {i+1} failed: {e}")
            if i < max_retries - 1:
                print("Retrying...")
            else:
                print("Max retries reached. Giving up.")
                raise
    return None
</code>`<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">

ملاحظة: هذا الكود بس للمثال، لا تستخدمه كذا في الإنتاج بدون تحسينات!

مشكلة الـ Retries المباشرة: الـ "Thundering Herd"

تخيل آلاف المستخدمين أو الخدمات يحاولون يتصلون بنفس الـ API، وكلهم يفشلون بنفس الوقت. لو كلهم يحاولون مرة ثانية بنفس اللحظة، وش بيصير؟ بيزيد الضغط على الـ API أكثر وأكثر، وممكن يطيح زيادة! كأنك قاعد تصرخ على واحد تعبان أصلاً.

الحل: الـ Backoff (التراجع الذكي)

هنا يجي دور الـ Backoff. الفكرة بسيطة: لا تحاول مرة ثانية فوراً. انتظر شوي، وبعدين حاول. وإذا فشلت، انتظر أكثر في المحاولة اللي بعدها. هذا يعطي الـ API فرصة يلتقط أنفاسه ويتعافى.

الـ Exponential Backoff: التراجع الأسي

هذي أشهر طريقة. تبدأ بمدة انتظار قصيرة، وبعد كل فشل، تضاعف المدة. مثلاً: 1 ثانية، 2 ثانية، 4 ثواني، 8 ثواني، وهكذا.

</code>`<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">python
import requests
import time

def call_api_exponential_backoff(url, max_retries=5, initial_delay=1):
    delay = initial_delay
    for i in range(max_retries):
        try:
            response = requests.get(url)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            print(f"Attempt {i+1} failed: {e}")
            if i < max_retries - 1:
                print(f"Retrying in {delay} seconds...")
                time.sleep(delay)
                delay *= 2 # Exponential increase
            else:
                print("Max retries reached. Giving up.")
                raise
    return None
</code>`<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">

الـ Jitter: إضافة عشوائية

حتى مع الـ Exponential Backoff، لو كل عملائك بدأوا المحاولة في نفس اللحظة (مثلاً بعد 1 ثانية، ثم 2 ثانية)، ممكن يرجعون يسوون "Thundering Herd".

الحل: نضيف "Jitter" (عشوائية) على وقت الانتظار. بدل ما ننتظر 2 ثانية بالضبط، ننتظر بين 1.5 و 2.5 ثانية مثلاً. هذا يوزع المحاولات بشكل أفضل ويقلل الضغط.

فيه أنواع للـ Jitter:

  • Full Jitter: تنتظر بين 0 والمدة الكاملة (الـ delay المحسوبة).
  • Decorrelated Jitter: تضيف عشوائية أكبر وتزيد المدة بشكل غير منتظم.

</code>`<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">python
import requests
import time
import random

def call_api_exponential_backoff_with_jitter(url, max_retries=5, initial_delay=1, max_delay=60):
    delay = initial_delay
    for i in range(max_retries):
        try:
            response = requests.get(url)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            print(f"Attempt {i+1} failed: {e}")
            if i < max_retries - 1:
                # Calculate jittered delay
                jittered_delay = random.uniform(0, min(delay, max_delay))
                print(f"Retrying in {jittered_delay:.2f} seconds (base delay: {delay})...")
                time.sleep(jittered_delay)
                delay = min(delay * 2, max_delay) # Exponential increase, capped
            else:
                print("Max retries reached. Giving up.")
                raise
    return None
</code>`<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">

نقاط مهمة لازم تتذكرها

  • Idempotency (العمليات غير المتكررة): تأكد إن العملية اللي قاعد تحاول تسويها آمنة للتكرار. يعني لو أرسلت طلب مرتين بالغلط، ما يسوي مشكلة (مثلاً: إضافة سجل جديد مرتين). عمليات GET و PUT عادةً idempotent، لكن POST ممكن ما تكون.
  • Rate Limiting: بعض الـ APIs عندها حد لعدد الطلبات في فترة معينة. الـ Retries ممكن تزيد المشكلة لو ما انتبهت للـ retry-after header اللي ممكن يرسله الـ API.
  • Circuit Breaker (قاطع الدائرة): إذا فشل الـ API لفترة طويلة أو عدد كبير من المرات، يمكن أفضل إنك توقف محاولات الاتصال فيه تماماً لفترة معينة (تفتح "الدائرة"). هذا يمنعك من إغراق الـ API بطلبات فاشلة ويخليك تحافظ على مواردك.
  • Max Retries & Max Delay: لازم تحدد عدد محاولات قصوى ووقت انتظار أقصى عشان ما يقعد تطبيقك يحاول للأبد.
  • Logging: سجل كل محاولة فشل وإعادة محاولة عشان تعرف وش قاعد يصير.

الخلاصة

الـ Retries والـ Backoff أدوات أساسية لأي مبرمج يتعامل مع الـ APIs. استخدامها بشكل صحيح يخلي تطبيقك أكثر مرونة واستقرار في مواجهة المشاكل المؤقتة. لا تنسَ تضيف الـ Jitter` عشان تقلل الضغط على الـ API وتتجنب مشكلة الـ "Thundering Herd".

أتمنى الدرس كان مفيد وخفيف! بالتوفيق!