API Tutorials

Başarısız CAPTCHA görevleri için Dead Letter Queue (DLQ)

Toplu CAPTCHA çözümünde tek bir görev sessizce kaybolduğunda, bunu çoğu zaman günler sonra eksik verilerden fark edersiniz. Dead letter queue (DLQ) tam olarak bu boşluğu kapatır: bir görev tüm yeniden denemelerden sonra hâlâ çözülemediyse, onu düşürmek yerine ayrı bir kuyruğa alır. Böylece başarısız görevleri daha sonra yeniden deneyebilir, hata dağılımını analiz edebilir veya uyarı tetikleyebilirsiniz — hiçbir iş sessizce kaybolmaz.

Örneğin gece boyunca bir staging ödeme akışını test eden bir otomasyon geliştiricisini düşünün. Yüzlerce QA senaryosunun içinde birkaç reCAPTCHA v2 çözümü zaman aşımına uğrarsa, DLQ olmadan bu görevler yalnızca bir günlük satırı bırakır ve sabah rapor eksik çıkar. DLQ ile aynı görevler sabah otomatik olarak yeniden işlenir.

DLQ'yu iş akışınıza en baştan ekleyin. Başarısızlıkları sonradan yakalamaya çalışmak üretimde çok daha maliyetlidir — çünkü hangi görevlerin sessizce kaybolduğunu artık geriye dönük olarak bilemezsiniz.


Bir görev neden DLQ'ya düşer?

Bir CAPTCHA görevinin DLQ'da sonuçlanmasının en yaygın nedenleri şunlardır:

  • ERROR_CAPTCHA_UNSOLVABLE – Çözücü doğrulamayı tamamlayamadı
  • ERROR_NO_SLOT_AVAILABLE – Tüm worker'lar meşgul, yeniden denemeler tükendi
  • Zaman aşımı – Çözücü, son tarih içinde bir yanıt döndürmedi
  • Ağ hataları – Sorgulama sırasında bağlantı koptu

Bir DLQ olmadan bu hatalar tek bir log satırı üretir ve unutulur. Amaç, bu görevleri kalıcı bir kayıp yerine ele alınabilir bir kuyruğa taşımaktır.


Python: yeniden denemeli bellek içi DLQ

Aşağıdaki uygulama, başarısız görevleri bir deque içinde tutar ve her denemeyi üstel geri çekilme (exponential backoff) ile ayırır. Ana çözüm döngüsü tüm denemeleri tükettiğinde, görev veri kaybı olmadan DLQ'ya eklenir:

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()}")

Beklenen çıktı:

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

DLQ size: 2

Beş görevden ikisi başarısız oldu ve normalde kaybolacaktı; bunun yerine ikisi de DLQ'ya alındı ve sonraki adımda yeniden denenmeye hazır.


DLQ'daki görevleri yeniden deneme

Ana toplu işlem bittikten sonra kuyruğu boşaltın. Buradaki püf nokta, sonsuz döngüyü önlemektir: her görev için task.attempts değerini kontrol edin ve toplam deneme eşiği aşıldığında görevi kalıcı olarak başarısız kabul edin.

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)

Bu ikinci turu genellikle bir zamanlanmış görev (cron) olarak, ana toplu işlemden birkaç dakika sonra çalıştırmak iyi sonuç verir — geçici zaman aşımları ve kısa süreli slot doluluğu o aralıkta çözülmüş olur.


JavaScript: dosyaya yazan kalıcı DLQ

Uzun süre çalışan bir Node.js servisinde bellek içi kuyruk yeterli değildir: süreç yeniden başladığında kuyruktaki görevler yok olur. Aşağıdaki sürüm, her push ve pop işleminden sonra kuyruğu diske yazar; böylece bir yeniden başlatma görev kaybına yol açmaz.

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()}`);
})();

Yüksek eşzamanlılıkta aynı dosyaya birden fazla süreç yazacaksa, dosya kilitleme ekleyin veya bir sonraki adımda Redis'e geçin.

Dosya tabanlı kalıcılık tek bir makinede güvenilirdir; iş yükünü birden fazla sunucuya dağıttığınızda paylaşımlı bir kuyruk (örneğin Redis) neredeyse her zaman daha temiz ölçeklenir.


DLQ verilerini analiz etme

DLQ yalnızca bir güvenlik ağı değildir; aynı zamanda değerli bir hata veri kaynağıdır. Görevleri JSON olarak dışa aktarın ve hata dağılımını sayın:

# 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}")

Bu dağılımı okuyarak birkaç somut aksiyon çıkarabilirsiniz:

  1. Sürekli başarısız olan sitekey'leri tespit edin ve googlekey ile pageurl parametrelerinin doğru olduğunu kontrol edin.
  2. Belirli saatlere yığılan zaman aşımlarını API yük profiliyle karşılaştırın.
  3. Ağ hataları yoğunsa proxy yapılandırmanızın sağlığını gözden geçirin.

Üretime almadan önce DLQ kontrol listesi

DLQ'yu canlı bir iş akışına bağlamadan önce aşağıdaki adımları sırayla doğrulayın:

  1. Ana çözüm döngüsünün her kalıcı başarısızlıkta görevi gerçekten DLQ'ya ittiğini bir test senaryosuyla teyit edin.
  2. retry_dlq() çağrısını ana toplu işten ayrı, zamanlanmış bir görev olarak planlayın.
  3. Toplam deneme eşiğini (max_retries artı DLQ yeniden denemeleri) sabitleyin ki hiçbir görev sonsuza kadar dönüp durmasın.
  4. Kuyruk boyutu için bir uyarı eşiği belirleyin ve bu eşik aşıldığında bir bildirim tetikleyin.
  5. Başarısız görev kayıtlarını düzenli olarak dışa aktarıp hata dağılımını gözden geçirin.

Bu listeyi dağıtım sürecinizin bir adımına dönüştürün: DLQ tahliyesi planlanmadan hiçbir toplu iş üretime çıkmasın.


Sorun giderme

Sorun Sebep Çözüm
DLQ sürekli büyüyor, hiç küçülmüyor Yeniden denemeler işlenmiyor retry_dlq() ile periyodik DLQ tahliyesini planlayın
Aynı görev sonsuza kadar yeniden deneniyor Maksimum deneme sınırı yok Yeniden kuyruğa almadan önce task.attempts değerini kontrol edin
DLQ dosyası bozuluyor Eşzamanlı yazma Dosya kilitleme kullanın veya Redis/veritabanına geçin
Çökme sırasında görevler kayboluyor Yalnızca bellek içi DLQ Dosya tabanlı veya Redis destekli DLQ kullanın

Sık sorulan sorular

DLQ ile normal yeniden deneme mantığı arasındaki fark nedir?

Yeniden deneme mantığı bir görevi anında birkaç kez dener; DLQ ise bu denemeler tükendikten sonra devreye girer. DLQ, "şimdi çözemedik" durumunu "kalıcı olarak kaybettik" durumuna dönüştürmeden ikinci bir şans katmanı sağlar.

DLQ'yu dosyayla mı yoksa Redis/veritabanı ile mi saklamalıyım?

Kısa ömürlü betikler için bellek içi kuyruk yeterlidir. Yeniden başlatmanın kuyruğu kaybedeceği uzun süre çalışan servislerde dosya tabanlı ya da Redis destekli bir DLQ kullanın; birden fazla worker aynı kuyruğu paylaşıyorsa Redis tercih edin.

Başarısız görevler CaptchaAI bakiyemi tüketir mi?

CaptchaAI thread bazlı faturalandırır ve her plan thread başına sınırsız çözüm içerir; bu yüzden başarısız denemeler için ayrı bir ücret yoktur. BASIC ($15/ay, 5 thread) planında bile yalnızca eşzamanlı thread sayısı için ödeme yaparsınız, çözüm başına değil.

DLQ'daki görevler için nasıl uyarı kurarım?

Bir görev DLQ'ya eklendiğinde push içinde bir eşik kontrolü çalıştırın: kuyruk boyutu belirli bir sayıyı aşarsa bir webhook veya bildirim gönderin. Bu, sessiz birikimleri fark etmenizi sağlar. Bir kesinti sırasında istekleri durdurmak için bunu Devre Kesici Kalıbı ile birleştirebilirsiniz.


CaptchaAI ile başarısız görevleri güvenle yakalayın

Ücretsiz API anahtarınızı captchaai.com üzerinden alın, yukarıdaki DLQ kodunu iş akışınıza ekleyin ve hiçbir başarısız CAPTCHA görevini bir daha kaybetmeyin.


İlgili kılavuzlar

Bu makale için yorumlar devre dışı bırakılmıştır.