دروس API

نمط قاطع الدائرة لاستدعاءات CAPTCHA API

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

تخيّل متجراً إلكترونياً في منطقة الخليج يشغّل اختبارات QA آلية على صفحات تسجيل محمية بـ reCAPTCHA v2. في ذروة موسم التخفيضات ترتفع الأحمال، فتبدأ نقطة نهاية الحل بإرجاع ERROR_NO_SLOT_AVAILABLE. من دون قاطع دائرة، يظل السكربت يرسل عشرات الطلبات الفاشلة ويستطلع نتائج لن تصل أبداً. أما مع قاطع الدائرة، فتُفتح الدائرة بعد بضع محاولات، وتتوقف الطلبات الجديدة، ويلتقط النظام أنفاسه حتى يستقر مزوّد الخدمة.


لماذا تحتاج استدعاءات CAPTCHA API إلى قاطع دائرة

يعتمد CaptchaAI نموذج تسعير قائماً على الـ Threads المتزامنة لا على عدد عمليات الحل؛ أي أن كل مهمة قيد التنفيذ تشغل خيطاً (Thread) طوال مدتها. وعندما تتعطل الخدمة وتستمر أنت في إرسال الطلبات، تدفع الثمن على ثلاث جبهات:

  • حجز الخيوط: كل محاولة فاشلة تشغل خيطاً محكوماً عليه بالفشل.
  • تأخير المهام السليمة الواقفة خلفها في قائمة الانتظار.
  • استطلاع بلا فائدة ينتظر نتائج لن تصل أبداً.

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


حالات قاطع الدائرة الثلاث

يعمل قاطع الدائرة كآلة حالات بسيطة تنتقل بين ثلاث حالات:

  1. مغلقة (Closed) — التشغيل الطبيعي. تمر جميع الطلبات إلى الخدمة، ويُحتسب كل فشل. وطالما بقي عدد حالات الفشل تحت العتبة المحددة، تظل الدائرة مغلقة.
  2. مفتوحة (Open) — تجاوز عدد حالات الفشل العتبة. تُرفض كل الطلبات فوراً دون حتى محاولة الاتصال بالخدمة، ما يوفّر الوقت والخيوط المهدورة. وتبقى الدائرة مفتوحة طوال مهلة الاسترداد.
  3. نصف مفتوحة (Half-Open) — بعد انقضاء مهلة الاسترداد، يُسمح بمرور طلب اختباري واحد فقط. فإن نجح، عادت الدائرة إلى الحالة المغلقة واستأنف النظام عمله الكامل؛ وإن فشل، أُعيد فتح الدائرة وبدأت مهلة استرداد جديدة.

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


تنفيذ قاطع الدائرة في Python

الصنف التالي يغلّف أي دالة تستدعي واجهة CAPTCHA API. لاحظ استخدام threading.Lock لحماية الحالة المشتركة أثناء التشغيل متعدد الخيوط، وهو تفصيل يُغفل كثيراً فيتسبب في حالات سباق يصعب تتبعها:

import time
import threading
import requests

SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
API_KEY = "YOUR_API_KEY"


class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = 0
        self.state = "closed"  # closed, open, half-open
        self._lock = threading.Lock()

    def call(self, func, *args, **kwargs):
        with self._lock:
            if self.state == "open":
                if time.time() - self.last_failure_time > self.recovery_timeout:
                    self.state = "half-open"
                    print("[circuit] State: half-open — testing one request")
                else:
                    remaining = self.recovery_timeout - (
                        time.time() - self.last_failure_time
                    )
                    raise CircuitOpenError(
                        f"Circuit open — retry in {remaining:.0f}s"
                    )

        try:
            result = func(*args, **kwargs)
            with self._lock:
                self.failure_count = 0
                if self.state == "half-open":
                    print("[circuit] State: closed — API recovered")
                self.state = "closed"
            return result
        except Exception as e:
            with self._lock:
                self.failure_count += 1
                self.last_failure_time = time.time()
                if self.failure_count >= self.failure_threshold:
                    self.state = "open"
                    print(
                        f"[circuit] State: open — "
                        f"{self.failure_count} failures"
                    )
            raise


class CircuitOpenError(Exception):
    pass


def solve_captcha(sitekey, page_url):
    resp = requests.post(SUBMIT_URL, data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": page_url,
        "json": "1",
    }, timeout=15)
    data = resp.json()
    if data["status"] != 1:
        raise Exception(f"Submit error: {data['request']}")

    task_id = data["request"]
    for _ in range(24):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": "1",
        }, timeout=15).json()
        if poll["status"] == 1:
            return poll["request"]
        if poll["request"] != "CAPCHA_NOT_READY":
            raise Exception(f"Poll error: {poll['request']}")
    raise TimeoutError(f"Task {task_id} timed out")


# Usage
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30)

for i in range(10):
    try:
        token = breaker.call(
            solve_captcha, "6Le-SITEKEY", "https://example.com"
        )
        print(f"[task-{i}] Solved: {token[:40]}...")
    except CircuitOpenError as e:
        print(f"[task-{i}] Skipped: {e}")
    except Exception as e:
        print(f"[task-{i}] Failed: {e}")

الناتج المتوقع:

[task-0] Solved: 03AGdBq26ZfPxL...
[task-1] Solved: 03AGdBq27AbCdE...
[task-2] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-3] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-4] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[circuit] State: open — 3 failures
[task-5] Skipped: Circuit open — retry in 28s
[task-6] Skipped: Circuit open — retry in 25s
...
[circuit] State: half-open — testing one request
[task-8] Solved: 03AGdBq28FgHiJ...
[circuit] State: closed — API recovered

هنا ضُبطت العتبة على ثلاث حالات فشل ومهلة استرداد قدرها 30 ثانية. بعد ثلاث رسائل ERROR_NO_SLOT_AVAILABLE متتالية تُفتح الدائرة، فتُتخطى المهام من 5 إلى 7 فوراً دون أي استدعاء للشبكة. وعند انقضاء المهلة تنتقل الدائرة إلى الحالة نصف المفتوحة، فينجح طلب الاختبار وتُغلق الدائرة ليعود المسار إلى طبيعته. ولفهم بقية رموز الأخطاء التي قد تُشغّل الدائرة، راجع مرجع رموز أخطاء CaptchaAI وطرق إصلاحها.


تنفيذ قاطع الدائرة في JavaScript

النسخة نفسها بمنطق غير متزامن (async) تناسب بيئات Node.js. المبدأ متطابق: تتبّع عدد الأخطاء، وافتح الدائرة عند تجاوز العتبة، ثم اسمح بطلب اختباري واحد بعد المهلة:

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.recoveryTimeout = options.recoveryTimeout || 60000;
    this.failureCount = 0;
    this.lastFailureTime = 0;
    this.state = 'closed';
  }

  async call(fn, ...args) {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
        this.state = 'half-open';
        console.log('[circuit] State: half-open');
      } else {
        const remaining = this.recoveryTimeout - (Date.now() - this.lastFailureTime);
        throw new Error(`Circuit open — retry in ${Math.ceil(remaining / 1000)}s`);
      }
    }

    try {
      const result = await fn(...args);
      this.failureCount = 0;
      if (this.state === 'half-open') {
        console.log('[circuit] State: closed — recovered');
      }
      this.state = 'closed';
      return result;
    } catch (error) {
      this.failureCount++;
      this.lastFailureTime = Date.now();
      if (this.failureCount >= this.failureThreshold) {
        this.state = 'open';
        console.log(`[circuit] State: open — ${this.failureCount} failures`);
      }
      throw error;
    }
  }
}

// Usage
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';
const breaker = new CircuitBreaker({ failureThreshold: 3, recoveryTimeout: 30000 });

async function solveCaptcha(sitekey, pageurl) {
  const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
    params: { key: API_KEY, method: 'userrecaptcha', googlekey: sitekey, pageurl, json: 1 }
  });

  if (submit.data.status !== 1) throw new Error(submit.data.request);
  const taskId = submit.data.request;

  for (let i = 0; i < 24; i++) {
    await new Promise(r => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: 1 }
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
  }
  throw new Error('Timeout');
}

(async () => {
  for (let i = 0; i < 10; i++) {
    try {
      const token = await breaker.call(solveCaptcha, '6Le-SITEKEY', 'https://example.com');
      console.log(`[task-${i}] Solved: ${token.substring(0, 40)}...`);
    } catch (err) {
      console.log(`[task-${i}] ${err.message}`);
    }
  }
})();

في بيئة Node.js أحادية الخيط لا تحتاج إلى قفل صريح كما في Python، لأن حلقة الأحداث تعالج تحديثات الحالة تسلسلياً. لكن إن وزّعت الحمل على عدة عمليات (Workers)، فكل عملية تحتفظ بحالة قاطع دائرة مستقلة، وقد ترغب حينها في مشاركة الحالة عبر مخزن مركزي مثل Redis حتى تفتح الدائرة وتُغلق على مستوى النظام كله لا داخل كل عملية بمفردها.


ضبط عتبات قاطع الدائرة

لا توجد قيم مثالية واحدة؛ فالأرقام المناسبة تعتمد على حجم حركة المرور لديك وحساسية سير العمل:

المعلمة حركة مرور منخفضة — أقل من 10/دقيقة حركة مرور عالية — أكثر من 100/دقيقة
failure_threshold 3 10
recovery_timeout 30 ثانية 60 ثانية

القاعدة العملية: اجعل حد الفشل مرتفعاً بما يكفي لتحمّل الأخطاء العابرة — فمهلة واحدة أو رمز CAPCHA_NOT_READY عابر لا ينبغي أن يُسقط الدائرة — لكن منخفضاً بما يكفي لإيقاف استنزاف خدمة متعطلة فعلاً.

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


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

إعادة المحاولة وقاطع الدائرة يكمّل أحدهما الآخر ولا يتعارضان. القاعدة: ضع منطق إعادة المحاولة داخل قاطع الدائرة، بحيث لا يحتسب قاطع الدائرة إلا حالات الفشل النهائية — تلك التي بقيت بعد استنفاد كل المحاولات:

def solve_with_retry(sitekey, page_url, max_retries=2):
    for attempt in range(max_retries + 1):
        try:
            return solve_captcha(sitekey, page_url)
        except Exception:
            if attempt == max_retries:
                raise
            time.sleep(2 ** attempt)

# Circuit breaker wraps the retry function
token = breaker.call(solve_with_retry, "6Le-SITEKEY", "https://example.com")

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


استكشاف الأخطاء وإصلاحها

المشكلة السبب الحل
تُفتح الدائرة بسرعة مفرطة العتبة منخفضة أكثر من اللازم ارفع قيمة failure_threshold
الدائرة لا تتعافى أبداً مهلة recovery_timeout طويلة جداً خفّضها إلى 30–60 ثانية
حالة سباق عند التشغيل متعدد الخيوط لا يوجد قفل على الحالة استخدم threading.Lock في Python أو عمليات ذرية
حظر كل الطلبات أثناء عطل جزئي قاطع واحد لكل نقاط النهاية افصل قاطعاً لنقطة الإرسال وآخر للاستطلاع

مراقبة قاطع الدائرة في الإنتاج

قاطع الدائرة الصامت خطر خفي: قد يبقى مفتوحاً ساعات دون أن ينتبه أحد. سجّل كل انتقال بين الحالات — كما تفعل عبارات print في المثال — وأرسل تنبيهاً فور دخول الحالة المفتوحة. وتابع ثلاثة مقاييس أساسية:

  • عدد مرات فتح الدائرة خلال اليوم.
  • متوسط مدة بقائها في الحالة المفتوحة.
  • نسبة الطلبات المرفوضة إلى إجمالي الطلبات.

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


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

ما الفرق بين قاطع الدائرة وإعادة المحاولة؟

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

هل يؤثر قاطع الدائرة على فاتورة CaptchaAI؟

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

كيف أتأكد أن قاطع الدائرة يعمل فعلاً؟

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

ماذا أفعل عندما تكون الدائرة مفتوحة؟

ضع مهمة CAPTCHA في قائمة الانتظار لمعالجتها لاحقاً، أو اعرض واجهة بديلة، أو تخطَّ العملية بأمان. راجع التدهور اللطيف عند فشل الحل.


ابنِ سير عمل CAPTCHA يصمد أمام الأعطال مع CaptchaAI

احصل على مفتاح API الخاص بك من captchaai.com وطبّق قاطع الدائرة على استدعاءاتك اليوم.


أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.