Eğitimler

CAPTCHA Çözme Güvenilirliği için Hata Bütçesi Takibi

CAPTCHA çözümleme hattınız %95 başarı oranını hedefliyor. Geçen hafta bu oran %94,2 idi. Bu bir sorun mu? Hata bütçesi olmadan bu soruyu niceliksel olarak yanıtlayamazsınız. Hata bütçesi, güvenilirlik SLO'nuzun altına düşmeden önce tam olarak kaç hataya tahammül edebileceğinizi ve bütçe bittiğinde ne yapacağınızı söyler.

Hata Bütçesi Temelleri

Konsept Tanım Örnek
SLO Hedef başarı oranı %95 başarılı çözümler
Bütçe hatası İzin verilen başarısızlık oranı Toplam çözümlerin %5'i başarısız olabilir
Yakma hızı Bütçenin ne kadar hızlı tüketildiği 2× bütçenin yarı yarıya tükendiği anlamına gelir
Pencere Ölçüm periyodu 24 saat veya 7 gün devam ediyor

SLO'nuz 10.000 çözümle 24 saatlik pencerede %95'e ulaşırsa hata bütçeniz 500 hata olur. 500 hataya ulaştığınızda yeni dağıtımlar veya riskli değişiklikler durdurulmalıdır.

Python: Hata Bütçesi Takibi

import time
import threading
from dataclasses import dataclass, field
from collections import deque
from enum import Enum

API_KEY = "YOUR_API_KEY"


class BudgetStatus(Enum):
    HEALTHY = "healthy"          # Budget > 50% remaining
    WARNING = "warning"          # Budget 10-50% remaining
    CRITICAL = "critical"        # Budget < 10% remaining
    EXHAUSTED = "exhausted"      # Budget depleted


@dataclass
class SLOConfig:
    """Service Level Objective configuration."""
    target_success_rate: float = 0.95  # 95%
    window_seconds: int = 86400        # 24 hours
    warning_threshold: float = 0.50    # Alert at 50% budget
    critical_threshold: float = 0.10   # Alert at 10% budget


@dataclass
class ErrorBudgetEvent:
    timestamp: float
    success: bool


class ErrorBudgetTracker:
    """Tracks error budget consumption for CAPTCHA solving."""

    def __init__(self, config: SLOConfig = SLOConfig()):
        self.config = config
        self._events: deque[ErrorBudgetEvent] = deque()
        self._lock = threading.Lock()
        self._callbacks: dict[BudgetStatus, list[callable]] = {
            status: [] for status in BudgetStatus
        }
        self._last_status = BudgetStatus.HEALTHY

    def on_status_change(self, status: BudgetStatus, callback: callable):
        """Register a callback for status transitions."""
        self._callbacks[status].append(callback)

    def record(self, success: bool):
        """Record a solve attempt."""
        now = time.monotonic()
        event = ErrorBudgetEvent(timestamp=now, success=success)

        with self._lock:
            self._events.append(event)
            self._prune(now)
            new_status = self._compute_status()

            if new_status != self._last_status:
                self._last_status = new_status
                for cb in self._callbacks.get(new_status, []):
                    try:
                        cb(self.get_report())
                    except Exception as e:
                        print(f"[BUDGET] Callback error: {e}")

    def _prune(self, now: float):
        """Remove events outside the window."""
        cutoff = now - self.config.window_seconds
        while self._events and self._events[0].timestamp < cutoff:
            self._events.popleft()

    def _compute_status(self) -> BudgetStatus:
        remaining = self.remaining_fraction
        if remaining <= 0:
            return BudgetStatus.EXHAUSTED
        if remaining < self.config.critical_threshold:
            return BudgetStatus.CRITICAL
        if remaining < self.config.warning_threshold:
            return BudgetStatus.WARNING
        return BudgetStatus.HEALTHY

    @property
    def total_events(self) -> int:
        with self._lock:
            return len(self._events)

    @property
    def success_count(self) -> int:
        with self._lock:
            return sum(1 for e in self._events if e.success)

    @property
    def failure_count(self) -> int:
        with self._lock:
            return sum(1 for e in self._events if not e.success)

    @property
    def current_success_rate(self) -> float:
        total = self.total_events
        return self.success_count / total if total > 0 else 1.0

    @property
    def error_budget_total(self) -> float:
        """Total allowed failures in the window."""
        total = self.total_events
        if total == 0:
            return 0
        return total * (1 - self.config.target_success_rate)

    @property
    def error_budget_remaining(self) -> float:
        """Remaining failure allowance."""
        return max(0, self.error_budget_total - self.failure_count)

    @property
    def remaining_fraction(self) -> float:
        """Fraction of error budget remaining (0.0 to 1.0)."""
        budget = self.error_budget_total
        if budget <= 0:
            return 1.0 if self.failure_count == 0 else 0.0
        return max(0, self.error_budget_remaining / budget)

    @property
    def burn_rate(self) -> float:
        """How fast the budget is being consumed (1.0 = normal, 2.0 = 2× faster)."""
        total = self.total_events
        if total == 0:
            return 0.0
        expected_failures = total * (1 - self.config.target_success_rate)
        if expected_failures == 0:
            return 0.0
        return self.failure_count / expected_failures

    def get_report(self) -> dict:
        return {
            "status": self._last_status.value,
            "slo_target": self.config.target_success_rate,
            "current_rate": round(self.current_success_rate, 4),
            "total_events": self.total_events,
            "successes": self.success_count,
            "failures": self.failure_count,
            "budget_total": round(self.error_budget_total, 1),
            "budget_remaining": round(self.error_budget_remaining, 1),
            "budget_remaining_pct": round(self.remaining_fraction * 100, 1),
            "burn_rate": round(self.burn_rate, 2),
        }


# --- Integration with solver ---

budget = ErrorBudgetTracker(SLOConfig(
    target_success_rate=0.95,
    window_seconds=3600,  # 1-hour window for demo
))

# Register alerts
budget.on_status_change(BudgetStatus.WARNING, lambda r:
    print(f"[ALERT] Budget warning: {r['budget_remaining_pct']}% remaining"))

budget.on_status_change(BudgetStatus.CRITICAL, lambda r:
    print(f"[ALERT] Budget critical: {r['budget_remaining_pct']}% remaining"))

budget.on_status_change(BudgetStatus.EXHAUSTED, lambda r:
    print(f"[ALERT] Budget EXHAUSTED — throttle new requests"))


def solve_with_budget(params: dict) -> str:
    """Solve CAPTCHA while tracking error budget."""
    import requests

    if budget._last_status == BudgetStatus.EXHAUSTED:
        raise RuntimeError("Error budget exhausted — solving paused")

    try:
        submit_params = {**params, "key": API_KEY, "json": 1}
        resp = requests.post(
            "https://ocr.captchaai.com/in.php", data=submit_params, timeout=30
        ).json()
        if resp.get("status") != 1:
            budget.record(False)
            raise RuntimeError(f"Submit: {resp.get('request')}")

        task_id = resp["request"]
        start = time.monotonic()
        while time.monotonic() - start < 180:
            time.sleep(5)
            poll = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY, "action": "get", "id": task_id, "json": 1,
            }, timeout=15).json()

            if poll.get("request") == "CAPCHA_NOT_READY":
                continue
            if poll.get("status") == 1:
                budget.record(True)
                return poll["request"]

            budget.record(False)
            raise RuntimeError(f"Solve: {poll.get('request')}")

        budget.record(False)
        raise RuntimeError("Timeout")

    except Exception:
        budget.record(False)
        raise


# Usage
for i in range(100):
    try:
        token = solve_with_budget({
            "method": "turnstile",
            "sitekey": "0x4XXXXXXXXXXXXXXXXX",
            "pageurl": "https://example.com",
        })
    except RuntimeError as e:
        if "exhausted" in str(e):
            print(f"Stopped at iteration {i}")
            break

print(budget.get_report())

JavaScript: Hata Bütçesi Takibi

class ErrorBudgetTracker {
  #events = [];
  #config;
  #callbacks = {};

  constructor(config = {}) {
    this.#config = {
      targetRate: config.targetRate || 0.95,
      windowMs: config.windowMs || 3600_000,
      warningThreshold: config.warningThreshold || 0.5,
      criticalThreshold: config.criticalThreshold || 0.1,
    };
    this.lastStatus = "healthy";
  }

  on(status, callback) {
    this.#callbacks[status] = this.#callbacks[status] || [];
    this.#callbacks[status].push(callback);
  }

  record(success) {
    const now = Date.now();
    this.#events.push({ time: now, success });
    this.#prune(now);

    const newStatus = this.#computeStatus();
    if (newStatus !== this.lastStatus) {
      this.lastStatus = newStatus;
      for (const cb of this.#callbacks[newStatus] || []) {
        cb(this.report());
      }
    }
  }

  #prune(now) {
    const cutoff = now - this.#config.windowMs;
    while (this.#events.length && this.#events[0].time < cutoff) {
      this.#events.shift();
    }
  }

  #computeStatus() {
    const frac = this.remainingFraction;
    if (frac <= 0) return "exhausted";
    if (frac < this.#config.criticalThreshold) return "critical";
    if (frac < this.#config.warningThreshold) return "warning";
    return "healthy";
  }

  get total() { return this.#events.length; }
  get successes() { return this.#events.filter((e) => e.success).length; }
  get failures() { return this.#events.filter((e) => !e.success).length; }
  get currentRate() { return this.total ? this.successes / this.total : 1; }

  get budgetTotal() {
    return this.total * (1 - this.#config.targetRate);
  }

  get budgetRemaining() {
    return Math.max(0, this.budgetTotal - this.failures);
  }

  get remainingFraction() {
    const bt = this.budgetTotal;
    if (bt <= 0) return this.failures === 0 ? 1 : 0;
    return Math.max(0, this.budgetRemaining / bt);
  }

  get burnRate() {
    const expected = this.total * (1 - this.#config.targetRate);
    return expected > 0 ? this.failures / expected : 0;
  }

  report() {
    return {
      status: this.lastStatus,
      currentRate: Math.round(this.currentRate * 10000) / 10000,
      total: this.total,
      failures: this.failures,
      budgetRemainingPct: Math.round(this.remainingFraction * 1000) / 10,
      burnRate: Math.round(this.burnRate * 100) / 100,
    };
  }
}

// Usage
const budget = new ErrorBudgetTracker({ targetRate: 0.95, windowMs: 3600_000 });

budget.on("warning", (r) => console.log(`[WARN] ${r.budgetRemainingPct}% budget left`));
budget.on("exhausted", (r) => console.log("[ALERT] Budget exhausted!"));

// Record results from your solver
budget.record(true);   // success
budget.record(false);  // failure
console.log(budget.report());

Yanma Hızı Uyarıları

Yanma oranı Anlamı Eylem
< 1,0 Beklenenden daha yavaş tüketmek Hiçbir eyleme gerek yok
1.0 Pencere ucunda egzoz temposunda Yakından izleyin
2.0 Bütçe yarı yarıya tükendi Araştırın ve yavaşlayın
5.0+ Hızlı bütçe tüketimi Kritik olmayan çözümleri duraklatın

Sorun giderme

Sorun Sebep Düzeltme
Bütçe çok çabuk tükendi SLO gerçek koşullar için çok sıkı Geçmiş verilere dayalı gerçekçi bir SLO belirleyin
Bütçe hiç tüketilmedi SLO çok cömert Güvenilirlik iyileştirmelerini desteklemek için SLO'yu sıkılaştırın
Durumlar arasındaki durum kapakları Pencere çok kısa Daha uzun bir ölçüm aralığı kullanın (24 saat vs 1 saat)
Düşük ses seviyesinde yanma hızı yanıltıcı Birkaç olay hesaplamayı çarpıtıyor Yazma oranını hesaplamadan önce minimum olay sayısını zorunlu kıl
Bütçe takip belleği büyüyor Budanmayan olaylar _prune'nin her record() çağrısında çalıştığını doğrulayın

SSS

CAPTCHA çözümü için gerçekçi bir SLO nedir?

CAPTCHA türüne bağlıdır. reCAPTCHA v2 genellikle %90-95 çözüm oranlarına ulaşır. Turnstile daha yüksek olabilir. Resim CAPTCHA'ları farklılık gösterir. Mevcut başarı oranınızı ölçerek başlayın, ardından anlamlı bir hata bütçesi oluşturmak için SLO'nuzu bu temel çizginin %2-3 altına ayarlayın.

Hata bütçesi tükendiğinde ne olmalı?

En az agresiften en agresif olana kadar seçenekler: ekibi uyarın, yeni istekleri azaltın, gerekli olmayan çözümleri duraklatın, manuel CAPTCHA işlemeye geçin. Tükenmiş bir bütçeyi asla sessizce görmezden gelmeyin.

Birden fazla CAPTCHA türünde hata bütçesini nasıl yönetirim?

Tür başına ayrı bütçeleri izleyin. reCAPTCHA'nın SLO'su %93 iken Turnstile'nin SLO'su %97'dir. Bunları tek bir bütçede toplamak, türe özgü sorunları gizler.

İlgili Makaleler

Sonraki Adımlar

CAPTCHA çözme güvenilirliğinizi niceliksel olarak takip edin -CaptchaAI API anahtarınızı alınve hata bütçesi takibini uygulayın.

İlgili kılavuzlar:

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