AI Agents · درس

تحديد معدل الطلبات ومنطق إعادة المحاولة

التراجع الأسي، والتعامل مع 429، واستخدام API بطريقة تراعي الخوادم

الدرس 4 من 413 خطوة

تحديد معدل الطلبات ومنطق إعادة المحاولة درس مجاني في AI Agents على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Agents، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Agents 4 دروس في المجموع.

ما تقييد معدل الطلبات؟

تقييد معدل الطلبات هو الطريقة التي تحمي بها واجهات API نفسها من التعرض لضغط يفوق طاقتها. فعندما يرسل وكيلك عددًا كبيرًا جدًا من الطلبات بسرعة مفرطة، تعيد واجهة API الاستجابة 429 Too Many Requests. وتشمل الحدود الشائعة عدد الطلبات في الثانية أو الدقيقة أو اليوم.

ويؤدي تجاهل حدود المعدل إلى حظر الوكلاء وإلغاء مفاتيح API وفرض رسوم إضافية.

import requests

response = requests.get(
    'https://api.example.com/data',
    headers={'Authorization': 'Bearer YOUR_KEY'}
)

if response.status_code == 429:
    print('Rate limit exceeded!')
    # Check headers for limit details
    limit = response.headers.get('X-RateLimit-Limit')
    remaining = response.headers.get('X-RateLimit-Remaining')
    reset = response.headers.get('X-RateLimit-Reset')
    print(f'Limit: {limit}, Remaining: {remaining}, Reset: {reset}')

رأس Retry-After

عندما تعيد واجهة API الحالة 429، فإنها غالبًا ما تتضمن رأس Retry-After الذي يحدد بالضبط عدد الثواني التي يجب انتظارها قبل إعادة المحاولة. احترم هذا الرأس دائمًا، فتجاهله وإعادة المحاولة فورًا سيؤديان إلى الحصول على 429 أخرى.

import requests
import time

def request_with_retry_after(url, headers):
    response = requests.get(url, headers=headers)

    if response.status_code == 429:
        retry_after = int(response.headers.get('Retry-After', 60))
        print(f'Rate limited. Waiting {retry_after} seconds...')
        time.sleep(retry_after)

        # Retry once after waiting
        response = requests.get(url, headers=headers)

    response.raise_for_status()
    return response.json()

التراجع الأسي

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

الصيغة: wait = 2 ** attempt

import requests
import time

def get_with_exponential_backoff(url, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers, timeout=(5, 30))

        if response.status_code == 200:
            return response.json()

        if response.status_code in (429, 500, 502, 503):
            wait = 2 ** attempt  # 1, 2, 4, 8, 16 seconds
            print(f'Attempt {attempt+1} failed ({response.status_code}). '
                  f'Waiting {wait}s before retry...')
            time.sleep(wait)
        else:
            response.raise_for_status()  # non-retryable error

    raise Exception(f'Failed after {max_retries} retries')

إضافة Jitter إلى التراجع

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

import requests
import time
import random

def get_with_jittered_backoff(url, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers, timeout=(5, 30))

        if response.status_code == 200:
            return response.json()

        if response.status_code in (429, 500, 502, 503):
            base_wait = 2 ** attempt
            # Add random jitter: actual wait is 50%-100% of base
            jitter = random.uniform(0.5, 1.0)
            wait = base_wait * jitter
            print(f'Waiting {wait:.1f}s (attempt {attempt+1})')
            time.sleep(wait)
        else:
            response.raise_for_status()

    raise Exception(f'Failed after {max_retries} retries')

مكتبة tenacity

تُعد tenacity مكتبة Python الأكثر شيوعًا لمنطق إعادة المحاولة. فهي تتعامل مع التراجع الأسي وJitter والحد الأقصى لعدد المحاولات وشروط التوقف المخصصة باستخدام صيغة مزخرف واضحة. وهي أكثر موثوقية بكثير من حلقات إعادة المحاولة المكتوبة يدويًا.

from tenacity import (
    retry, stop_after_attempt, wait_exponential,
    retry_if_exception_type, before_sleep_log
)
import requests
import logging

logger = logging.getLogger(__name__)

@retry(
    stop=stop_after_attempt(5),
    wait=wait_exponential(multiplier=1, min=2, max=60),
    retry=retry_if_exception_type(requests.exceptions.HTTPError),
    before_sleep=before_sleep_log(logger, logging.WARNING)
)
def fetch_data(url, headers):
    response = requests.get(url, headers=headers, timeout=(5, 30))
    if response.status_code == 429:
        response.raise_for_status()  # triggers retry
    response.raise_for_status()
    return response.json()

tenacity مع شرط مخصص لإعادة المحاولة

يمكنك تعليم tenacity إعادة المحاولة فقط عند رموز حالة محددة، مثل 429 و5xx، والتوقف فورًا عند أخطاء العميل (4xx) التي لن تستفيد من إعادة المحاولة. استخدم retry_if_result أو دالة قابلة للاستدعاء مخصصة لفحص الاستجابة.

from tenacity import (
    retry, stop_after_attempt, wait_exponential,
    retry_if_result
)
import requests

def is_retryable_response(response):
    return response.status_code in (429, 500, 502, 503, 504)

@retry(
    stop=stop_after_attempt(4),
    wait=wait_exponential(multiplier=2, min=2, max=30),
    retry=retry_if_result(is_retryable_response)
)
def resilient_get(url, headers):
    response = requests.get(url, headers=headers, timeout=(5, 30))
    return response  # retry logic inspects the response object

# Usage
response = resilient_get(
    'https://api.example.com/data',
    {'Authorization': 'Bearer YOUR_KEY'}
)
data = response.json()

الإدارة الاستباقية لحدود المعدل

أفضل استراتيجية هي تجنب بلوغ حدود المعدل من الأساس. افحص رؤوس حدود المعدل في كل استجابة وأبطئ الإرسال عندما تقترب من الحد. وتتضمن العديد من واجهات API الرأسين X-RateLimit-Remaining وX-RateLimit-Reset.

import requests
import time

class RateLimitAwareClient:
    def __init__(self, base_url, api_key):
        self.base_url = base_url
        self.headers = {'Authorization': f'Bearer {api_key}'}
        self.remaining = 1000  # assume generous limit

    def get(self, path):
        # Proactively slow down if nearly exhausted
        if self.remaining < 10:
            print('Rate limit nearly exhausted, sleeping 5s...')
            time.sleep(5)

        response = requests.get(
            f'{self.base_url}{path}', headers=self.headers
        )

        # Update remaining from response headers
        remaining_str = response.headers.get('X-RateLimit-Remaining')
        if remaining_str:
            self.remaining = int(remaining_str)

        response.raise_for_status()
        return response.json()

الحد الأقصى للمحاولات والاستسلام

يجب أن يكون لمنطق إعادة المحاولة حد دائمًا. فقد تؤدي إعادة المحاولة إلى ما لا نهاية إلى أعطال متسلسلة، حيث يعلق جميع وكلائك في حلقات إعادة المحاولة. بعد max_retries، ارفع استثناءً نهائيًا يتضمن سياقًا حول ما فشل، لكي يتمكن الوكيل من تسجيله والانتقال إلى أعمال أخرى.

import requests
import time

class MaxRetriesExceeded(Exception):
    def __init__(self, url, attempts, last_status):
        self.url = url
        self.attempts = attempts
        self.last_status = last_status
        super().__init__(
            f'Failed {url} after {attempts} attempts '
            f'(last status: {last_status})'
        )

def fetch_with_limit(url, headers, max_retries=3):
    last_response = None
    for attempt in range(max_retries):
        last_response = requests.get(url, headers=headers)
        if last_response.status_code == 200:
            return last_response.json()
        time.sleep(2 ** attempt)
    raise MaxRetriesExceeded(url, max_retries, last_response.status_code)

نمط قاطع الدائرة

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

import time

class CircuitBreaker:
    CLOSED, OPEN, HALF_OPEN = 'closed', 'open', 'half_open'

    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.state = self.CLOSED
        self.failures = 0
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.opened_at = None

    def call(self, func, *args, **kwargs):
        if self.state == self.OPEN:
            if time.time() - self.opened_at > self.recovery_timeout:
                self.state = self.HALF_OPEN
            else:
                raise Exception('Circuit OPEN — service unavailable')
        try:
            result = func(*args, **kwargs)
            self.failures = 0
            self.state = self.CLOSED
            return result
        except Exception as e:
            self.failures += 1
            if self.failures >= self.failure_threshold:
                self.state = self.OPEN
                self.opened_at = time.time()
                print(f'Circuit OPENED after {self.failures} failures')
            raise

# --- demo ---
def flaky():
    raise ValueError('upstream 500')

def works():
    return 'ok'

cb = CircuitBreaker(failure_threshold=3, recovery_timeout=60)
for i in range(3):
    try:
        cb.call(flaky)
    except Exception as e:
        print(f'call {i+1} failed: {e}')
print(f'Breaker state after 3 failures: {cb.state}')
try:
    cb.call(flaky)
except Exception as e:
    print(f'Rejected without calling flaky(): {e}')

وضع الطلبات في قائمة انتظار للبقاء ضمن الحدود

بالنسبة إلى الوكلاء الذين يجرون العديد من الاستدعاءات ضمن دفعة واحدة، استخدم دلو الرموز أو محدد معدل بسيطًا يعتمد على sleep للبقاء ضمن الحدود. احسب الفاصل الآمن بين الاستدعاءات بناءً على حد معدل واجهة API، مثلًا: 60 استدعاءً في الدقيقة = استدعاء واحد في الثانية.

import requests
import time

def batch_requests(urls, headers, calls_per_minute=60):
    interval = 60.0 / calls_per_minute  # seconds between calls
    results = []

    for i, url in enumerate(urls):
        start = time.time()

        response = requests.get(url, headers=headers, timeout=(5, 30))
        response.raise_for_status()
        results.append(response.json())

        print(f'Processed {i+1}/{len(urls)}')

        # Sleep for remaining time in the interval
        elapsed = time.time() - start
        sleep_time = interval - elapsed
        if sleep_time > 0:
            time.sleep(sleep_time)

    return results

دمج منطق إعادة المحاولة مع رؤوس التراجع

يجمع النمط الأكثر متانة بين أوقات الانتظار التي يحددها الخادم (Retry-After) والتراجع الأسي كخيار احتياطي. فضّل دائمًا إرشادات الخادم عندما تكون متاحة، فهو يعرف بالضبط متى يمكنك إعادة المحاولة.

import requests
import time
import random

def smart_retry(url, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers, timeout=(5, 30))

        if response.status_code == 200:
            return response.json()

        if response.status_code == 429:
            # Use Retry-After if provided, else exponential backoff
            retry_after = response.headers.get('Retry-After')
            if retry_after:
                wait = int(retry_after)
            else:
                wait = (2 ** attempt) + random.uniform(0, 1)
            print(f'429 rate limit. Waiting {wait:.1f}s...')
            time.sleep(wait)

        elif response.status_code >= 500:
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f'Server error {response.status_code}. Waiting {wait:.1f}s...')
            time.sleep(wait)

        else:
            response.raise_for_status()  # non-retryable

    raise Exception(f'Gave up after {max_retries} attempts')

اختبار سريع: التراجع الأسي

اختبر مدى فهمك لاستراتيجيات إعادة المحاولة.

مراجعة تقييد معدل الطلبات وإعادة المحاولة

أصبح بإمكان وكلائك الآن التعامل مع حدود المعدل بسلاسة:

  • 429 Too Many Requests — احترم رأس Retry-After وانتظر قبل إعادة المحاولة
  • التراجع الأسي — تضاعف wait = 2^attempt مدة الانتظار مع كل إعادة محاولة
  • Jitter — يضيف عشوائية لتوزيع عمليات إعادة المحاولة بين مثيلات الوكلاء المتعددة
  • tenacity — يتولى كل منطق إعادة المحاولة باستخدام المزخرفات وإعدادات واضحة
  • قاطع الدائرة — يوقف إغراق الخدمة المتعطلة بالطلبات بعد بلوغ حد معين
  • تحديد المعدل استباقيًا — افحص X-RateLimit-Remaining وأبطئ الإرسال قبل بلوغ الحد
البدء مجانًا

تعلم AI Agents مع معلم ذكاء اصطناعي — مجانًا

اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.

الدورات
60
الدروس
239

الأسئلة الشائعة

هل درس «تحديد معدل الطلبات ومنطق إعادة المحاولة» مجاني؟

نعم — نص درس «تحديد معدل الطلبات ومنطق إعادة المحاولة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Agents، انتقل إلى CoddyKit PRO. تتضمن دورة AI Agents 4 دروس في المجموع.

ماذا ستتعلم في «تحديد معدل الطلبات ومنطق إعادة المحاولة»؟

التراجع الأسي، والتعامل مع 429، واستخدام API بطريقة تراعي الخوادم تتمرن على AI Agents مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Agents؟

لا تُشترط خبرة سابقة. AI Agents على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «تحديد معدل الطلبات ومنطق إعادة المحاولة»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Agents هذا؟

نعم. كل درس في AI Agents يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. أساسيات REST API لمطوري الوكلاء
  2. المصادقة: مفاتيح API وOAuth
  3. التعامل مع استجابات API وأخطائها
  4. تحديد معدل الطلبات ومنطق إعادة المحاولة
← العودة إلى AI Agents