تشغيل الطلبات بالتوازي

ما ستتعلمه

كيفية تشغيل دفعة كبيرة من طلبات FourA بشكل متزامن دون تلقي أخطاء 429، وذلك عبر تحديد سقف لعدد الاتصالات المفتوحة والاستجابة لرفض الطلب بدلا من تكراره مباشرة.

المتطلبات الأساسية

الحدود التي تعمل ضمنها

تتضمن خطتك حدين أقصيين لكل endpoint: عدد الطلبات المسموح بتشغيلها في الوقت نفسه، وعدد الطلبات المسموح ببدئها في الدقيقة (يمتلك Browser حصة يومية بدلا من الحصة لكل دقيقة). لكل من Single وProxy وBrowser أرقامه الخاصة، وتوضح علامة تبويب Limits & Features في Usage & Limits هذه القيم.

الطلب الذي يتجاوز الحد الأقصى للتزامن يعود برمز HTTP 429 مصحوبا بـ X-FourA-Limit: plan_limit_concurrency مع توضيح الحد الأقصى في نص الاستجابة:

{
  "error": "Concurrency limit reached: your plan allows 50 simultaneous single request(s). Retry when an in-flight request finishes.",
  "reason": "plan_limit_concurrency",
  "documentation": "https://foura.ai/prices",
  "limit": 50,
  "in_flight": 51,
  "retry_after_seconds": 1
}

الطلب الذي يتجاوز الحد الأقصى للدقيقة يرجع برمز X-FourA-Limit: plan_limit_rate، ويحدد retry_after_seconds المدة المتبقية حتى يُسمح بطلب إضافي (من 1 إلى 120 ثانية).

كلا الرفضين فوري. لا تضع FourA الطلب في قائمة انتظار لإعادته لاحقًا، لذلك لا يتم استهلاك أي رصيد ولا تتم فوترة أي شيء. لا يُحتسب الطلب المرفوض ضمن الدقيقة المنزلقة، لذا فإن الحساب الذي يطلب بمعدل أعلى قليلًا من حده الأقصى يستمر في الاستفادة من الحد الأقصى بالكامل. عاصفة إعادة المحاولة (retry storm) لها استجابة خاصة: عندما تستمر الطلبات المرفوضة في الوصول بمعدل يتجاوز ضعف الحد الأقصى للدقيقة، يُرفض كل طلب على ذلك الـ endpoint برمز cooldown: true وretry_after_seconds: 30، لذا توقف مؤقتًا خلال تلك الـ 30 ثانية. مرجع الحقول الكامل: Rate Limits.

أمران مهمان غالبًا ما يغفل عنهما المستخدمون:

  • طلب POST /api/auto/ لا يحجز خانة بذاته، ولكن كل طلب فرعي من نوع Single وProxy وBrowser ينفذه نيابة عنك يحجز خانة. يمكن لطلب auto واحد أن يحجز أكثر من خانة أثناء تشغيل تسلسله التدريجي. (في لوحة التحكم الخاصة بك، طلب auto هو الـ request؛ وطلباته الفرعية هي محاولاته.)
  • يشغل طلب Browser خانته طوال المدة التي تستغرقها الصفحة في المعالجة (render)، وهي مدة أطول بكثير من طلب Single. تملأ دفعة من طلبات browser حدها الأقصى بعدد طلبات أقل من دفعة طلبات single.

الخطوة 1: حدد التزامن الخاص بك

اختر رقمًا أقل من الحد الأقصى للـ endpoint الذي تستدعيه والتزم به. تنجز مجموعة الـ workers ذلك في سطر واحد:

import os
import concurrent.futures
import requests

API = "https://eu.api.foura.ai/api/single/"
KEY = os.environ["FOURA_API_KEY"]
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}

# Below your plan's Single concurrency, so a slow request never pushes the batch over it.
MAX_IN_FLIGHT = 30

def fetch_one(url):
    resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
    return url, resp.status_code, resp.json()

def fetch_all(urls):
    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_IN_FLIGHT) as pool:
        for outcome in concurrent.futures.as_completed(pool.submit(fetch_one, u) for u in urls):
            results.append(outcome.result())
    return results

max_workers هي الآلية بالكامل. لا يفتح المجمع أبدا اتصالات أكثر من هذا العدد، لذا يمكن أن تحتوي الدفعة على مليون URL وتظل دون الحد المسموح.

اترك هامشا إضافيا. إذا كانت هناك عمليتان من جانبك تستخدمان الحساب نفسه، حتى مع مفاتيح API مختلفة، فإنهما تشتركان في حد أقصى واحد، لذا خصص النصف لكل منهما. إذا كان الهدف السريع يتيح للمجمع إنهاء الـ requests في وقت أسرع مما تسمح به الدقيقة الواحدة، فاضبط وتيرة الـ workers لتظل دون الحد الأقصى للدقيقة أيضا.

الخطوة 2: التراجع بدلا من إعادة الإرسال

إذا ورد رفض، فإن التصرف الخاطئ هو إعادة إرسال الدفعة فورا. سيتم رفض كل استدعاء فيها مجددا، وستتراكم عمليات إعادة المحاولة فوق الـ requests التي كانت قيد التشغيل بالفعل.

انتظر أولا. فترة الانتظار محددة في الـ header ‏Retry-After وفي retry_after_seconds داخل الـ body:

import time

# Plan limits that no short wait will clear.
STOP_ON = {"plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", "plan_limit_feature", "plan_limit_premium"}

def fetch_one(url, attempts=4):
    for attempt in range(attempts):
        resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)

        limit = resp.headers.get("X-FourA-Limit")
        if limit in STOP_ON:
            raise RuntimeError(f"stopped by {limit}")

        if resp.status_code not in (429, 503):
            return url, resp.status_code, resp.json()

        header = resp.headers.get("Retry-After")
        if header and header.isdigit():
            wait = int(header)
        else:
            body = resp.json()
            wait = body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
        time.sleep(wait)

    return url, 429, {"error": "still refused after retries"}

أضف خفقانا عشوائيا (jitter) عند تشغيل العديد من الـ workers. بدونه، يستيقظ كل worker تم رفضه في نفس الثانية في نفس اللحظة ويُرفض مجددا مع بقية الـ workers.

import random
time.sleep(wait + random.uniform(0, 0.5))

الخطوة 3: الأمر نفسه في Node

const API = 'https://eu.api.foura.ai/api/single/';
const HEADERS = {
  'X-API-Key': process.env.FOURA_API_KEY,
  'Content-Type': 'application/json',
};
const MAX_IN_FLIGHT = 30;

async function fetchOne(url) {
  const resp = await fetch(API, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({ method: 'GET', url }),
  });
  return { url, status: resp.status, body: await resp.json() };
}

async function fetchAll(urls) {
  const queue = [...urls];
  const results = [];

  async function worker() {
    while (queue.length) {
      results.push(await fetchOne(queue.pop()));
    }
  }

  await Promise.all(
    Array.from({ length: Math.min(MAX_IN_FLIGHT, urls.length) }, worker)
  );
  return results;
}

يضمن استخدام عدد ثابت من الـ workers التي تسحب من queue واحدة بقاء هذا العدد المحدد فقط من الاستدعاءات مفتوحا، أيا كان حجم الدفعة.

الخطوة 4: راقب ما تستخدمه فعليا

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

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

أخطاء شائعة

  • إرسال القائمة بأكملها دفعة واحدة. تشغيل asyncio.gather على 5,000 عنوان URL، أو Promise.all على مصفوفة غير مقيدة، يفتح 5,000 استدعاء. حدد سعة الـ pool، وليس القائمة.
  • إعادة المحاولة بالتوازي. إعادة إرسال كل استدعاء مرفوض فور رفضه يعيد إنتاج موجة الضغط المفاجئة التي سببت الرفض في الأصل. انتظر مدة Retry-After، وأضف تباينا عشوائيا (jitter).
  • التعامل مع الحصة اليومية أو الدورية على أنها مجرد انتظار. يتم تصفير plan_limit_browser_daily عند منتصف الليل بتوقيت UTC، وplan_limit_credits وplan_limit_bandwidth في نهاية دورة الفوترة. أوقف التشغيل واقرأ resets_at من الـ body عند توفره.
  • حساب استدعاءات auto كخانة واحدة لكل منها. التدرج الداخلي في /api/auto/ ينشئ استدعاءات فرعية حقيقية، وهي ما تحسبه الحدود القصوى.
  • مشاركة مفتاح عبر عمليات متعددة دون تقسيم الميزانية. الحدود القصوى هي لكل حساب، وليست لكل عملية.
  • تحديد حجم pool واحدة لجميع الـ endpoints. لكل من Single وProxy وBrowser رقم تزامن خاص بها. تحديد حجم pool بناء على Single سيتجاوز الحد الأقصى الأقل الخاص بـ Browser.
آخر تحديث: 15 أكتوبر 2026