Tutorial API

Graceful Degradation Saat CAPTCHA Solving Gagal

CAPTCHA solving bisa gagal — timeout, parameter buruk, saldo nol, atau rate limit. Jika otomasi Anda crash pada kegagalan pertama, Anda kehilangan semua progress. Graceful degradation membuat pipeline tetap berjalan: lewati, retry, masukkan ke queue, atau fallback ke alternatif.


Mode Kegagalan

Kegagalan Kode Error Strategi Pemulihan
Timeout CAPCHA_NOT_READY (melebihi polling) Retry dengan challenge baru
Parameter buruk ERROR_BAD_PARAMETERS Log dan lewati — perbaiki ekstraksi
Sitekey salah ERROR_WRONG_GOOGLEKEY Re-ekstrak sitekey
Saldo nol ERROR_ZERO_BALANCE Jeda, alert, tunggu top up
Rate limited ERROR_TOO_MUCH_REQUESTS Exponential backoff
API down Connection error Circuit breaker + retry

Pola 1: Skip dan Lanjutkan

Untuk operasi batch di mana kegagalan individual dapat diterima:

import requests
import time

API_KEY = "YOUR_API_KEY"


def solve_or_skip(captcha_type, sitekey, page_url, max_retries=2):
    """Try to solve; return None on failure instead of crashing."""
    for attempt in range(max_retries):
        try:
            token = solve_captcha(captcha_type, sitekey, page_url)
            if token:
                return token
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")

    return None  # Skip this item


def process_urls(urls):
    results = []
    skipped = []

    for url in urls:
        sitekey = extract_sitekey(url)
        if not sitekey:
            skipped.append({"url": url, "reason": "no_sitekey"})
            continue

        token = solve_or_skip("recaptcha_v2", sitekey, url)
        if token:
            data = submit_form(url, token)
            results.append({"url": url, "data": data})
        else:
            skipped.append({"url": url, "reason": "solve_failed"})

    print(f"Processed: {len(results)}, Skipped: {len(skipped)}")
    return results, skipped

Pola 2: Retry Queue

Task yang gagal dimasukkan ke retry queue untuk diproses nanti:

from collections import deque
import json

class RetryQueue:
    def __init__(self, max_retries=3, backoff_base=60):
        self.queue = deque()
        self.max_retries = max_retries
        self.backoff_base = backoff_base

    def add(self, task):
        task["retry_count"] = task.get("retry_count", 0) + 1
        if task["retry_count"] <= self.max_retries:
            task["retry_after"] = time.time() + (
                self.backoff_base * task["retry_count"]
            )
            self.queue.append(task)
            return True
        return False  # Exceeded max retries

    def get_ready(self):
        """Get tasks ready for retry."""
        ready = []
        remaining = deque()
        now = time.time()

        while self.queue:
            task = self.queue.popleft()
            if task["retry_after"] <= now:
                ready.append(task)
            else:
                remaining.append(task)

        self.queue = remaining
        return ready

    def save(self, filepath="retry_queue.json"):
        with open(filepath, "w") as f:
            json.dump(list(self.queue), f)

    def load(self, filepath="retry_queue.json"):
        try:
            with open(filepath) as f:
                self.queue = deque(json.load(f))
        except FileNotFoundError:
            pass


# Usage
retry_q = RetryQueue()

def process_with_retry(task):
    try:
        token = solve_captcha(task["type"], task["sitekey"], task["url"])
        if token:
            return submit_form(task["url"], token)
        else:
            retry_q.add(task)
    except Exception:
        retry_q.add(task)

# Process retry queue periodically
def drain_retry_queue():
    ready = retry_q.get_ready()
    for task in ready:
        process_with_retry(task)

Pola 3: Degraded Mode

Jika solving service tidak tersedia, alihkan ke mode terbatas:

class CaptchaSolver:
    def __init__(self, api_key):
        self.api_key = api_key
        self.degraded = False
        self.failure_count = 0
        self.failure_threshold = 5
        self.recovery_time = None

    def solve(self, captcha_type, sitekey, page_url):
        if self.degraded:
            if time.time() < self.recovery_time:
                return self._degraded_action(page_url)
            else:
                self.degraded = False
                self.failure_count = 0

        try:
            token = self._solve_api(captcha_type, sitekey, page_url)
            self.failure_count = 0
            return token
        except Exception as e:
            self.failure_count += 1
            if self.failure_count >= self.failure_threshold:
                self._enter_degraded_mode()
            raise

    def _enter_degraded_mode(self):
        self.degraded = True
        self.recovery_time = time.time() + 300  # 5 min
        print("Entering degraded mode for 5 minutes")
        # Send alert

    def _degraded_action(self, url):
        """What to do when solving is unavailable."""
        # Option A: Skip CAPTCHA pages entirely
        return None

        # Option B: Queue for later
        # retry_queue.add({"url": url, ...})
        # return None

        # Option C: Try alternative solver
        # return self._solve_with_backup_api(...)

    def _solve_api(self, captcha_type, sitekey, page_url):
        # Normal CaptchaAI API call
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": page_url,
            "json": "1",
        }).json()

        if resp["status"] != 1:
            raise Exception(resp["request"])

        task_id = resp["request"]
        for _ in range(24):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": "1"
            }).json()
            if result["status"] == 1:
                return result["request"]
            if result["request"] != "CAPCHA_NOT_READY":
                raise Exception(result["request"])

        raise Exception("TIMEOUT")

Node.js: Pola gabungan

class ResilientSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.retryQueue = [];
    this.failureCount = 0;
    this.degraded = false;
  }

  async solve(type, sitekey, pageUrl) {
    if (this.degraded) {
      this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
      return null;
    }

    try {
      const token = await this._callApi(type, sitekey, pageUrl);
      this.failureCount = 0;
      return token;
    } catch (err) {
      this.failureCount++;

      if (err.message === 'ERROR_ZERO_BALANCE') {
        this._enterDegraded(600000); // 10 min
        return null;
      }

      if (this.failureCount >= 5) {
        this._enterDegraded(300000); // 5 min
      }

      this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
      return null;
    }
  }

  _enterDegraded(durationMs) {
    this.degraded = true;
    console.warn(`Degraded mode for ${durationMs / 1000}s`);
    setTimeout(() => {
      this.degraded = false;
      this.failureCount = 0;
      this.drainRetryQueue();
    }, durationMs);
  }

  async drainRetryQueue() {
    const tasks = this.retryQueue.splice(0);
    for (const task of tasks) {
      await this.solve(task.type, task.sitekey, task.pageUrl);
    }
  }

  async _callApi(type, sitekey, pageUrl) {
    // Standard submit + poll
    const axios = require('axios');
    const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
      params: { key: this.apiKey, method: 'userrecaptcha', googlekey: sitekey, pageurl: 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: this.apiKey, 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');
  }
}

Matriks Keputusan Retry

  • Retry hanya jika kegagalan tampak sementara dan status sesi sekitarnya masih valid.
  • Replay langkah jika request dapat dibuat ulang secara deterministik tanpa menduplikasi tindakan user.
  • Skip atau jeda alur kerja jika percobaan lebih lanjut hanya akan membuang saldo atau meningkatkan risiko deteksi.

Pemecahan Masalah

Masalah Penyebab Solusi
Semua task dilewati Degraded mode aktif terlalu agresif Naikkan failure threshold
Retry queue terus bertambah Task tidak pernah berhasil Tetapkan max retry; pindah ke dead-letter queue
Pemulihan terlalu lambat Timeout degraded terlalu lama Kurangi recovery time; tambahkan health check probe
Task queue hilang saat restart Queue di memory Persiste queue ke file atau database

Pertanyaan Umum

Apa perbedaan graceful degradation dan circuit breaker?

Circuit breaker mencegah panggilan sepenuhnya saat kegagalan terdeteksi. Graceful degradation lebih luas — mencakup fallback behavior, skip logic, dan alur kerja alternatif. Keduanya bekerja baik bersama.

Haruskah saya selalu retry task yang gagal?

Tidak untuk ERROR_BAD_PARAMETERS atau ERROR_WRONG_GOOGLEKEY — keduanya tidak akan berhasil di-retry. Hanya retry error sementara seperti timeout dan rate limit.


Panduan Terkait

Komentar dinonaktifkan untuk artikel ini.