دروس API

قائمة انتظار الرسائل الميتة لمهام اختبار CAPTCHA الفاشلة

في أنظمة الأتمتة التي تعالج آلاف طلبات CAPTCHA يومياً، ليست المشكلة في المهمة التي تنجح، بل في المهمة التي تفشل بصمت ولا يلاحظها أحد. قائمة انتظار الرسائل الميتة (Dead-Letter Queue أو DLQ) هي المكان الذي تُحفظ فيه كل مهمة استنفدت محاولاتها، بدلاً من أن تتلاشى في سطر سجل منسي، فتستطيع إعادة معالجتها لاحقاً أو تحليل سبب فشلها أو إطلاق تنبيه قبل أن يتحول الخطأ الصغير إلى فجوة في بياناتك.

هنا نبني DLQ عملية فوق CaptchaAI API لحل reCAPTCHA v2 بنسختين: واحدة في الذاكرة بلغة Python وأخرى محفوظة على القرص بلغة JavaScript، ثم نضيف إعادة المحاولة والتحليل.


ما المقصود بقائمة انتظار الرسائل الميتة (DLQ)؟

قائمة انتظار الرسائل الميتة بنية تخزين منفصلة تستقبل المهام التي فشلت نهائياً بعد استنفاد محاولات إعادة المحاولة. الفكرة مستعارة من أنظمة الرسائل مثل RabbitMQ وAmazon SQS: بدلاً من حذف الرسالة التي تعذّر تسليمها، تُحوَّل إلى قائمة جانبية تُفحص وتُعالَج لاحقاً.

في سياق حل CAPTCHA، يحمل كل عنصر في القائمة ما يكفي من السياق لإعادة تشغيل المهمة من جديد:

  1. مفتاح الموقع (sitekey) الخاص بالصفحة المستهدفة.
  2. عنوان الصفحة التي ظهر عليها التحدي.
  3. رمز الخطأ الذي أنهى المحاولة الأخيرة.
  4. عدد المحاولات التي جرت قبل الفشل النهائي.
  5. الطابع الزمني للحظة الفشل.

هذا يفصل بين مسار النجاح السريع ومسار التعافي البطيء، فلا تتعطل معالجة الدُفعة الأساسية بسبب حفنة من المهام العنيدة.

ملاحظة: لا تصل مهمة إلى DLQ إلا بعد استنفاد كل محاولات إعادة المحاولة الفورية؛ فالقائمة ملاذ أخير للتعافي، لا بديل عنها.


لماذا تفشل مهام CAPTCHA وتصل إلى قائمة الانتظار

قبل بناء القائمة، من المفيد معرفة الأسباب الأكثر شيوعاً لوصول مهمة إلى DLQ:

  1. ERROR_CAPTCHA_UNSOLVABLE — تعذّر على الخدمة إكمال التحدي، وغالباً بسبب مفتاح موقع خاطئ أو صفحة تغيّرت بنيتها.
  2. ERROR_NO_SLOT_AVAILABLE — جميع الـ Threads المتاحة في خطتك مشغولة واستُنفدت محاولات إعادة الإرسال.
  3. انتهاء المهلة — لم تُرجع الخدمة نتيجة ضمن الموعد النهائي للاستطلاع الدوري.
  4. أخطاء الشبكة — انقطع الاتصال أثناء إرسال الطلب أو استطلاع النتيجة.

من دون DLQ، يتحول كل فشل إلى سطر في السجل ثم يُنسى، ومع الحجم الكبير تصبح هذه الحالات ثغرات حقيقية في نتائجك.


بناء قائمة انتظار الرسائل الميتة في الذاكرة مع Python

النسخة الأبسط تحتفظ بالمهام الفاشلة في الذاكرة باستخدام deque بحجم أقصى محدد. الفئة DeadLetterQueue تضيف المهمة عند فشلها النهائي، وتتيح سحبها وتصديرها إلى ملف JSON للتحليل. ودالة solve_captcha تحاول الحل مع تراجع أسي، ولا تدفع المهمة إلى القائمة إلا بعد استنفاد كل المحاولات:

import time
import json
import requests
from collections import deque
from dataclasses import dataclass, asdict
from typing import Optional

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


@dataclass
class FailedTask:
    sitekey: str
    page_url: str
    error: str
    attempts: int
    timestamp: float
    task_id: Optional[str] = None


class DeadLetterQueue:
    def __init__(self, max_size=1000, max_retries=3):
        self._queue = deque(maxlen=max_size)
        self.max_retries = max_retries

    def push(self, task: FailedTask):
        self._queue.append(task)
        print(f"[dlq] Added: {task.error} (attempts: {task.attempts})")

    def pop(self) -> Optional[FailedTask]:
        return self._queue.popleft() if self._queue else None

    def size(self) -> int:
        return len(self._queue)

    def peek_all(self) -> list:
        return [asdict(t) for t in self._queue]

    def export_json(self, path: str):
        with open(path, "w") as f:
            json.dump(self.peek_all(), f, indent=2)
        print(f"[dlq] Exported {self.size()} tasks to {path}")


dlq = DeadLetterQueue(max_retries=3)


def solve_captcha(sitekey, page_url, max_retries=3):
    for attempt in range(max_retries + 1):
        try:
            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(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(poll["request"])

            raise TimeoutError(f"Task {task_id} timed out")

        except Exception as e:
            if attempt == max_retries:
                dlq.push(FailedTask(
                    sitekey=sitekey,
                    page_url=page_url,
                    error=str(e),
                    attempts=attempt + 1,
                    timestamp=time.time(),
                ))
                return None
            time.sleep(2 ** attempt)

    return None


# Process a batch
urls = [f"https://example.com/page/{i}" for i in range(5)]
for url in urls:
    token = solve_captcha("6Le-SITEKEY", url)
    if token:
        print(f"Solved: {token[:40]}...")

print(f"\nDLQ size: {dlq.size()}")

عند تشغيل الدُفعة، يظهر لك سجل يوضّح المهام التي نجحت وتلك التي انتهت في القائمة:

Solved: 03AGdBq26ZfPxL...
Solved: 03AGdBq27AbCdE...
[dlq] Added: ERROR_CAPTCHA_UNSOLVABLE (attempts: 4)
Solved: 03AGdBq28FgHiJ...
[dlq] Added: Task 71823460 timed out (attempts: 4)

DLQ size: 2

في هذا المثال نجحت ثلاث مهام، بينما استقرّت مهمتان في DLQ: واحدة بسبب ERROR_CAPTCHA_UNSOLVABLE وأخرى بسبب انتهاء المهلة.


إعادة معالجة المهام من قائمة الانتظار

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

def retry_dlq(dlq: DeadLetterQueue, max_retries=2):
    retried = 0
    recovered = 0

    while dlq.size() > 0:
        task = dlq.pop()
        if task.attempts >= dlq.max_retries + max_retries:
            print(f"[dlq] Permanently failed: {task.sitekey} — {task.error}")
            continue

        retried += 1
        token = solve_captcha(
            task.sitekey, task.page_url, max_retries=max_retries
        )
        if token:
            recovered += 1
            print(f"[dlq-retry] Recovered: {token[:40]}...")

    print(f"[dlq] Retried: {retried}, Recovered: {recovered}")

# Run DLQ retry after main batch
retry_dlq(dlq)

تحكم هذه الدالة في دورة التعافي عبر ثلاث خطوات واضحة:

  1. اسحب المهمة من القائمة وتحقّق من إجمالي محاولاتها التراكمية.
  2. تجاهل نهائياً كل مهمة تجاوزت الحد الأقصى، وسجّلها للمراجعة اليدوية.
  3. أعد محاولة الباقي، وأحصِ ما تعافى فعلاً.

شغّل هذه الدالة بعد الدُفعة الأساسية أو ضمن مهمة مجدولة. المفتاح هو احترام حدّ المحاولات التراكمي (task.attempts) حتى لا تدور المهمة في حلقة لا تنتهي.


حفظ قائمة الانتظار على القرص باستخدام JavaScript

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

const fs = require('fs');
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';
const DLQ_FILE = './captcha-dlq.json';

class DeadLetterQueue {
  constructor(maxRetries = 3) {
    this.maxRetries = maxRetries;
    this.queue = this._load();
  }

  push(task) {
    this.queue.push({
      ...task,
      timestamp: Date.now(),
    });
    this._save();
    console.log(`[dlq] Added: ${task.error} (attempts: ${task.attempts})`);
  }

  pop() {
    const task = this.queue.shift();
    if (task) this._save();
    return task || null;
  }

  size() {
    return this.queue.length;
  }

  _load() {
    try {
      return JSON.parse(fs.readFileSync(DLQ_FILE, 'utf8'));
    } catch {
      return [];
    }
  }

  _save() {
    fs.writeFileSync(DLQ_FILE, JSON.stringify(this.queue, null, 2));
  }
}

const dlq = new DeadLetterQueue(3);

async function solveCaptcha(sitekey, pageurl, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      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(`Task ${taskId} timed out`);
    } catch (err) {
      if (attempt === maxRetries) {
        dlq.push({ sitekey, pageurl, error: err.message, attempts: attempt + 1 });
        return null;
      }
      await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
    }
  }
}

// Process tasks
(async () => {
  for (let i = 0; i < 5; i++) {
    const token = await solveCaptcha('6Le-SITEKEY', `https://example.com/page/${i}`);
    if (token) console.log(`Solved: ${token.substring(0, 40)}...`);
  }
  console.log(`DLQ size: ${dlq.size()}`);
})();

مع كل عملية push أو pop تُكتب القائمة كاملة إلى القرص، وهذا كافٍ للأحجام الصغيرة والمتوسطة. أما عند التزامن العالي بين عدة عمليات فالأفضل الانتقال إلى مخزن مشترك مثل Redis لتفادي تلف الملف.


تحليل أنماط الفشل واستخلاص الدروس

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

# Export DLQ for analysis
dlq.export_json("failed-tasks.json")

# Analyze error distribution
from collections import Counter
errors = Counter(t["error"] for t in dlq.peek_all())
for error, count in errors.most_common():
    print(f"  {error}: {count}")

استعمل هذه البيانات لاتخاذ قرارات ملموسة:

  1. مفاتيح مواقع تفشل باستمرار — تحقق من صحة الـ sitekey وعنوان الصفحة أولاً.
  2. تركّز حالات انتهاء المهلة في ساعات محددة — مؤشر ذروة تحميل؛ وزّع الطلبات أو ارفع عدد الـ Threads.
  3. كثرة أخطاء الشبكة — افحص سلامة الخادم الوسيط (البروكسي) واستقرار الاتصال.

سيناريو عملي: بوابة دفع في ذروة موسم التخفيضات

تخيّل منصة تجارة إلكترونية في منطقة الخليج تعالج عمليات تسجيل دخول وإتمام شراء آلية أثناء موسم تخفيضات. مع تضاعف حركة المرور ترتفع نسبة استجابات ERROR_NO_SLOT_AVAILABLE، لأن الـ Threads المخصصة في الخطة تُستهلك بالكامل خلال دقائق الذروة.

من دون DLQ، تفشل مئات مهام التحقق بصمت وتضيع طلبات حقيقية. أما مع DLQ فتُحفظ هذه المهام وتُعاد معالجتها تلقائياً بعد انحسار الذروة ببضع دقائق، عندما تتحرر الـ Threads مجدداً. وبدمج القائمة مع نمط قاطع الدائرة لاستدعاءات CAPTCHA API يمكنك إيقاف الإرسال مؤقتاً أثناء الضغط الشديد ثم استئنافه تلقائياً، فلا تفقد أي طلب ولا تُغرق الخدمة بمحاولات ميؤوس منها.

نصيحة تشغيلية: احتفظ بلوحة بسيطة تعرض حجم DLQ ومعدل التعافي لحظياً؛ فارتفاع الحجم مع انخفاض التعافي أوضح إشارة إلى أن المشكلة في بيئتك لا في المهام نفسها.


جدول استكشاف الأخطاء وحلولها

المشكلة السبب الحل
نمو القائمة بلا حدود لا تتم معالجة المهام المخزّنة جدولة تفريغ دوري للقائمة عبر retry_dlq()
إعادة محاولة المهمة نفسها بلا نهاية غياب حدّ أقصى للمحاولات تحقّق من task.attempts قبل إعادة الإدراج
تلف ملف DLQ كتابة متزامنة من عدة عمليات استخدم قفل الملف أو انتقل إلى Redis أو قاعدة بيانات
ضياع المهام عند التعطّل القائمة محفوظة في الذاكرة فقط استخدم قائمة مبنية على ملف أو مدعومة بـ Redis

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

ما الفرق بين حفظ DLQ في الذاكرة وحفظه بشكل دائم؟

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

هل تستهلك المهام المخزّنة في DLQ رصيد الـ Threads في CaptchaAI؟

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

كم مرة يجب أن أعيد محاولة المهمة قبل تجاهلها نهائياً؟

بعد محاولتين إلى ثلاث من داخل DLQ، فوق المحاولات الأصلية. إذا تجاوز إجمالي إخفاقات المهمة ست مرات، فالأرجح أن المشكلة في المعطيات نفسها — مثل sitekey خاطئ — لا في الخدمة؛ سجّل الحالة وتابع.

كيف أراقب حجم DLQ وأطلق تنبيهاً عند تضخّمه؟

اقرأ قيمة dlq.size() دورياً وأرسلها إلى نظام المراقبة لديك، وحدّد عتبة معقولة — مثل تجاوز القائمة 100 مهمة — تُطلق عندها تنبيهاً عبر بريد أو webhook. النمو المفاجئ مؤشر مبكر على عطل يستحق التدخل الفوري.

هل يمكنني استخدام Redis بدل الملف لقائمة موزّعة بين عدة عمّال؟

نعم، وهو الخيار المفضّل عند تشغيل عدة عمّال (workers) بالتوازي. يوفّر Redis عمليات ذرّية لإضافة المهام وسحبها، فيتجنّب تلف الملف الناتج عن الكتابة المتزامنة، ويسمح لكل العمّال بالمشاركة في تفريغ القائمة نفسها دون تعارض.


لا تدع أي مهمة CAPTCHA فاشلة تضيع بعد اليوم

أنشئ مفتاح CaptchaAI API مجاناً من captchaai.com وابدأ بتطبيق DLQ خلال دقائق.


أدلة ذات صلة

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