تشغيل الطلبات بالتوازي
ما ستتعلمه
كيفية تشغيل دفعة كبيرة من طلبات FourA بشكل متزامن دون تلقي أخطاء 429، عن طريق تحديد سقف لعدد الطلبات المفتوحة والاستجابة لرفض الطلب بدلا من تكراره.
المتطلبات الأساسية
- مفتاح FourA API (احصل على واحد من هنا)
- إصدار Python 3.9+ مع
requests، أو Node.js 18+
الحدود التي تعمل وفقها
تتضمن خطتك حدين أقصيين لكل 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 يمتد حتى نهاية الدقيقة.
كلا الرفضين فوري. لا يضع FourA الاستدعاء في قائمة انتظار ليعيده لاحقا، لذلك لا يتم استهلاك أي شيء ولا يتم احتساب أي تكلفة. ومع ذلك، تُحسب الاستدعاءات المرفوضة ضمن الدقيقة المنزلقة، لذا فإن عاصفة إعادة المحاولة (retry storm) تزيد من فترة التهدئة الخاصة بها. مرجع الحقول الكامل: Rate Limits.
أمران مهمان غالبا ما يغفل عنهما المستخدمون:
- لا يُحتسب استدعاء
POST /api/auto/بذاته، ولكن يُحتسب كل استدعاء فرعي من نوع Single و Proxy و Browser يجريه نيابة عنك. يمكن لاستدعاء auto واحد أن يشغل أكثر من فتحة واحدة أثناء تشغيل التسلسل الخاص به. - يشغل طلب Browser فتحته طوال المدة التي يستغرقها عرض الصفحة، وهي أطول بكثير من طلب Single. تملأ دفعة من استدعاءات browser حدها الأقصى بعدد طلبات أقل مقارنة بدفعة من استدعاءات single.
Step 1: Cap Your Own Concurrency
اختر رقما أقل من الحد الأقصى لـ endpoint الذي تستدعيه وثبته. يقوم worker pool بذلك في سطر واحد:
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 هي الآلية بأكملها. لا يحتوي الـ pool مطلقًا على عدد من الاستدعاءات المفتوحة يتجاوز هذا الحد، لذا يمكن أن تحتوي الدفعة على مليون URL وتظل ضمن الـ limit.
اترك مساحة احتياطية. إذا كانت هناك عمليتان من جانبك تتشاركان مفتاح API واحدًا، فإنهما تتشاركان حدًا أقصى واحدًا، لذا امنح كلًا منهما النصف. إذا كان الهدف سريعًا ويسمح للـ pool بإنهاء الـ requests بشكل أسرع مما تسمح به الدقيقة، فاضبط وتيرة الـ workers للبقاء تحت الحد الأقصى للدقيقة أيضًا.
Step 2: Back Off Instead of Re-Sending
إذا وصل رفض بالفعل، فإن التصرف الخاطئ هو إعادة إرسال الدفعة فورًا. سيتم رفض كل استدعاء فيها مجددًا، وستتراكم محاولات إعادة الإرسال فوق الـ 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 يتم رفضه في نفس الثانية سيستيقظ في نفس الثانية ويتم رفضها معًا.
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 واحدة بقاء هذا العدد المحدد فقط من الاستدعاءات مفتوحا، مهما كان حجم الـ batch.
الخطوة 4: مراقبة ما تستخدمه فعليا
افتح الاستخدام والحدود (Usage & Limits) في dashboard. تعرض العدادات المباشرة بجانب حدودك القصوى التزامن (concurrency)، والمعدل لكل دقيقة، وطلبات المتصفح يوميا أثناء تغيرها، حتى تتمكن من تحديد حجم MAX_IN_FLIGHT بناء على تشغيل فعلي بدلا من التخمين.
يسجل Activity Log حالات الرفض أيضا. إن عملية التشغيل التي انتهت بالعدد الصحيح من الصفوف ولكن مع تناثر لنتائج rate_limit كانت تخنق نفسها ذاتيا.
أخطاء شائعة
- إطلاق القائمة بالكامل دفعة واحدة. يؤدي استخدام
asyncio.gatherعلى 5000 عنوان URL، أوPromise.allعلى مصفوفة غير مقيدة، إلى فتح 5000 استدعاء. قم بتقييد الـ pool، وليس القائمة. - إعادة المحاولة بالتوازي. إعادة إرسال كل استدعاء تم رفضه فور رفضه يعيد إنتاج الاندفاع (burst) الذي تسبب في الرفض. انتظر مدة
Retry-After، وأضف jitter. - التعامل مع الحصة اليومية أو الدورية كفترة انتظار قصيرة. تتم إعادة تعيين
plan_limit_browser_dailyفي منتصف الليل بتوقيت UTC، وplan_limit_creditsوplan_limit_bandwidthفي نهاية دورة الفوترة. أوقف التشغيل واقرأresets_atمن الـ body حيثما وجد. - احتساب استدعاءات auto كخانة واحدة لكل منها. السلسلة المتدرجة داخل
/api/auto/تنشئ استدعاءات فرعية فعلية، وتلك هي ما تحسبه الحدود القصوى. - مشاركة المفتاح عبر عدة عمليات دون تقسيم الميزانية. الحدود القصوى تكون لكل حساب، وليس لكل عملية.
- تحديد حجم pool واحد لكل endpoint. كل من Single وProxy وBrowser يمتلك رقم التزامن الخاص به. إن pool المخصص لـ Single سيتجاوز الحد الأقصى الأصغر لـ Browser.
مواضيع ذات صلة
- Rate Limits: كل أشكال الرفض ومعنى كل حقل
- Response Headers:
X-FourA-LimitوRetry-After - Request Outcomes: لماذا لا يتم احتساب نتيجة
rate_limitفي الفاتورة أبدا - Usage & Limits: أين تجد العدادات المباشرة وأرقام خطتك
- Smart Fetch (Auto): كيف يتحول استدعاء auto واحد إلى عدة استدعاءات فرعية