DevOps & Scaling

CAPTCHA Çözme Worker Filolarında Kesintisiz Sürüm Güncellemesi

Yeni sürümü filoya tek hamlede basarsanız, o anda res.php üzerinde sonucu beklenen her görev çöpe gider. Doğru yöntem kademeli güncellemedir: worker'ları teker teker yeni görev almaz hâle getirin, üzerlerindeki aktif görevlerin bitmesini bekleyin, yeni sürümü dağıtın, sağlık kontrolünden geçirin ve ancak ondan sonra sıradaki worker'a geçin. Aşağıda bu döngünün çalışan orkestratör hâli var — Python ve JavaScript olarak.

Belirleyici detay şu: bir CAPTCHA görevi sıradan bir HTTP isteği kadar kısa sürmez. in.php'ye görevi gönderdikten sonra token'ı almanız onlarca saniye alabilir. Yani "worker'ı durdurdum" ile "worker gerçekten boş" arasında dakikalar olabilir; tahliye (drain) süresini bu gerçeğe göre ayarlamazsanız kesintisiz dağıtım kâğıt üzerinde kalır.

CAPTCHA worker'ları neden sıradan servislerden farklı davranır

Standart bir HTTP servisinde bir örneği kapatmadan önce beklemeniz gereken süre birkaç yüz milisaniyedir. CAPTCHA çözme worker'ında ise her aktif görev, uzak tarafta devam eden bir çözüm işidir. Süreci öldürdüğünüzde çözüm iptal olmaz, yalnızca sonucu kimse toplamaz: hedef sistemdeki form akışı yarıda kalır ve o thread meşgul kaldığı için kapasiteniz boşa gider.

CaptchaAI planları thread tabanlıdır — çözüm başına değil, eş zamanlı thread başına ödersiniz ve her plan ay boyunca thread başına sınırsız çözüm içerir. BASIC ($15/ay, 5 thread) ile çalışan küçük bir ekipte tek worker'ı kaybetmek eş zamanlı kapasitenin beşte biri demektir; ADVANCE ($90/ay, 50 thread) seviyesinde ise dağıtım stratejisi doğrudan bir maliyet konusudur, çünkü boşta dönen thread ödediğiniz kapasitedir. Kademeli güncelleme burada yalnızca hoş bir DevOps alışkanlığı değil, kapasite koruma yöntemidir.

Döngünün iskeleti: tahliye, dağıtım, sağlık, ilerleme

Workers: [W1-old] [W2-old] [W3-old] [W4-old]

Step 1:  [W1-drain] [W2-old]  [W3-old]  [W4-old]
Step 2:  [W1-NEW✓]  [W2-old]  [W3-old]  [W4-old]
Step 3:  [W1-NEW✓]  [W2-drain] [W3-old]  [W4-old]
Step 4:  [W1-NEW✓]  [W2-NEW✓]  [W3-old]  [W4-old]
  ...until all updated

Sıra sabittir ve kısayolu yoktur. Sağlık kontrolü başarısız olursa ilerleme durur ve o ana kadar güncellenmiş worker'lar geri alınır. Yarım kalmış bir filo, tamamı eski sürümde olan bir filodan daha risklidir: karışık sürümler hata ayıklamayı neredeyse imkânsız hâle getirir.

Python orkestratörü: durum makinesi ve tahliye mantığı

Aşağıdaki sınıf worker durumlarını (RUNNING, DRAINING, STOPPED, UPDATING) açıkça modelliyor. Görev yönlendirmesi yalnızca RUNNING durumundaki worker'lara yapıldığı için tahliyeye alınan worker'a yeni iş düşmüyor. drain() metodu aktif görev sayacı sıfırlanana kadar bekliyor; zaman aşımına ulaşırsa kalan görev sayısını raporlayıp devam ediyor.

import os
import time
import signal
import threading
import requests
from dataclasses import dataclass, field
from enum import Enum

API_KEY = os.environ["CAPTCHAAI_API_KEY"]


class WorkerState(Enum):
    RUNNING = "running"
    DRAINING = "draining"
    STOPPED = "stopped"
    UPDATING = "updating"


@dataclass
class Worker:
    worker_id: str
    version: str
    state: WorkerState = WorkerState.RUNNING
    active_tasks: int = 0
    tasks_completed: int = 0
    session: requests.Session = field(default_factory=requests.Session)

    def solve(self, task):
        if self.state != WorkerState.RUNNING:
            return {"error": "WORKER_NOT_ACCEPTING"}

        self.active_tasks += 1
        try:
            result = self._do_solve(task)
            self.tasks_completed += 1
            return result
        finally:
            self.active_tasks -= 1

    def _do_solve(self, task):
        resp = self.session.post("https://ocr.captchaai.com/in.php", data={
            "key": API_KEY,
            "method": task.get("method", "userrecaptcha"),
            "googlekey": task["sitekey"],
            "pageurl": task["pageurl"],
            "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            return {"error": data.get("request")}

        captcha_id = data["request"]
        for _ in range(60):
            time.sleep(5)
            result = self.session.get(
                "https://ocr.captchaai.com/res.php",
                params={
                    "key": API_KEY,
                    "action": "get",
                    "id": captcha_id,
                    "json": 1
                }
            ).json()
            if result.get("status") == 1:
                return {"solution": result["request"]}
            if result.get("request") != "CAPCHA_NOT_READY":
                return {"error": result.get("request")}
        return {"error": "TIMEOUT"}

    def drain(self, timeout=120):
        """Stop accepting tasks and wait for active tasks to complete."""
        self.state = WorkerState.DRAINING
        start = time.time()
        while self.active_tasks > 0:
            if time.time() - start > timeout:
                print(f"Worker {self.worker_id}: drain timeout with "
                      f"{self.active_tasks} tasks remaining")
                break
            time.sleep(1)
        self.state = WorkerState.STOPPED

    @property
    def is_healthy(self):
        return self.state == WorkerState.RUNNING


class RollingUpdateOrchestrator:
    def __init__(self, workers):
        self.workers = {w.worker_id: w for w in workers}
        self.lock = threading.Lock()

    def get_available_worker(self):
        """Route tasks only to RUNNING workers."""
        with self.lock:
            for worker in self.workers.values():
                if worker.state == WorkerState.RUNNING:
                    return worker
        return None

    def rolling_update(self, new_version, health_check_fn=None,
                       max_unavailable=1, drain_timeout=120):
        """Update workers one at a time with health gates."""
        worker_ids = list(self.workers.keys())
        updated = []
        failed = []

        for i in range(0, len(worker_ids), max_unavailable):
            batch = worker_ids[i:i + max_unavailable]

            for wid in batch:
                worker = self.workers[wid]
                print(f"[{wid}] Draining (v{worker.version})...")

                # Step 1: Drain active tasks
                worker.drain(timeout=drain_timeout)

                # Step 2: "Deploy" new version
                print(f"[{wid}] Deploying v{new_version}...")
                worker.state = WorkerState.UPDATING
                worker.version = new_version
                time.sleep(2)  # Simulate deployment

                # Step 3: Start and health check
                worker.state = WorkerState.RUNNING
                if health_check_fn:
                    healthy = health_check_fn(worker)
                    if not healthy:
                        print(f"[{wid}] Health check FAILED — rolling back")
                        failed.append(wid)
                        self._rollback(updated)
                        return {
                            "status": "rolled_back",
                            "failed_at": wid,
                            "updated": updated,
                        }

                updated.append(wid)
                print(f"[{wid}] Updated to v{new_version} ✓")

        return {"status": "complete", "updated": updated, "failed": failed}

    def _rollback(self, updated_ids):
        """Roll back already-updated workers."""
        for wid in updated_ids:
            worker = self.workers[wid]
            print(f"[{wid}] Rolling back...")
            worker.state = WorkerState.STOPPED
            time.sleep(1)
            worker.version = "rollback"
            worker.state = WorkerState.RUNNING

    @property
    def status(self):
        return {
            wid: {
                "version": w.version,
                "state": w.state.value,
                "active_tasks": w.active_tasks,
            }
            for wid, w in self.workers.items()
        }


# Create fleet
workers = [Worker(f"w{i}", "1.2.0") for i in range(6)]
orchestrator = RollingUpdateOrchestrator(workers)


def health_check(worker):
    """Verify worker can solve a test CAPTCHA."""
    # In production, send a real test task
    return worker.state == WorkerState.RUNNING


# Execute rolling update
result = orchestrator.rolling_update(
    new_version="1.3.0",
    health_check_fn=health_check,
    max_unavailable=1,
    drain_timeout=60
)
print(f"Rolling update result: {result}")

Üretimde health_check fonksiyonunu örnekteki hâliyle bırakmayın. En ucuz sağlık kontrolü bakiye sorgusudur; en anlamlısı, worker'ın gerçekten bir görev gönderip token alabildiğini doğrulayan tam bir çözüm turudur.

JavaScript tarafı: ilerleme takibi ve toplu iptal eşiği

Node.js sürümü aynı sırayı izler, üstüne iki şey ekler: ilerleme sayacı ve bir hata eşiği. Filonun dörtte birinden fazlası sağlık kontrolünden geçemezse güncelleme kendini durdurur — bu noktada sorun tek bir worker'da değil, yeni sürümün kendisindedir.

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

class RollingUpdater {
  constructor(workerCount, currentVersion) {
    this.workers = Array.from({ length: workerCount }, (_, i) => ({
      id: `worker-${i}`,
      version: currentVersion,
      state: "running",
      activeTasks: 0,
    }));
    this.progress = { total: workerCount, completed: 0, failed: 0 };
  }

  async update(newVersion, options = {}) {
    const {
      maxUnavailable = 1,
      drainTimeout = 60000,
      healthCheckRetries = 3,
    } = options;

    console.log(
      `Starting rolling update: v${this.workers[0].version} → v${newVersion}`
    );

    for (let i = 0; i < this.workers.length; i += maxUnavailable) {
      const batch = this.workers.slice(i, i + maxUnavailable);

      for (const worker of batch) {
        try {
          // Drain
          console.log(`[${worker.id}] Draining...`);
          worker.state = "draining";
          await this.waitForDrain(worker, drainTimeout);

          // Deploy
          console.log(`[${worker.id}] Deploying v${newVersion}...`);
          worker.state = "updating";
          worker.version = newVersion;

          // Health check
          worker.state = "running";
          const healthy = await this.healthCheck(worker, healthCheckRetries);

          if (!healthy) {
            worker.state = "failed";
            this.progress.failed++;
            console.log(`[${worker.id}] FAILED health check`);

            if (this.progress.failed > Math.floor(this.workers.length * 0.25)) {
              console.log("Too many failures — aborting rolling update");
              return { status: "aborted", progress: this.progress };
            }
            continue;
          }

          this.progress.completed++;
          console.log(
            `[${worker.id}] Updated ✓ (${this.progress.completed}/${this.progress.total})`
          );
        } catch (err) {
          console.error(`[${worker.id}] Error: ${err.message}`);
          this.progress.failed++;
        }
      }
    }

    return { status: "complete", progress: this.progress };
  }

  async waitForDrain(worker, timeout) {
    const start = Date.now();
    while (worker.activeTasks > 0 && Date.now() - start < timeout) {
      await new Promise((r) => setTimeout(r, 1000));
    }
  }

  async healthCheck(worker, retries) {
    for (let attempt = 0; attempt < retries; attempt++) {
      try {
        const resp = await axios.get("https://ocr.captchaai.com/res.php", {
          params: { key: API_KEY, action: "getbalance", json: 1 },
          timeout: 10000,
        });
        if (resp.data.status === 1) return true;
      } catch {
        // Retry
      }
      await new Promise((r) => setTimeout(r, 5000));
    }
    return false;
  }
}

// Execute
const updater = new RollingUpdater(8, "1.2.0");
updater
  .update("1.3.0", { maxUnavailable: 2, drainTimeout: 30000 })
  .then((result) => console.log("Result:", JSON.stringify(result, null, 2)));

Buradaki healthCheck, getbalance çağrısını kullanıyor: worker'ın API anahtarını okuyabildiğini ve ağ yolunun açık olduğunu doğrular. Yanlış ortam değişkeni ya da eksik anahtar gibi en sık görülen iki dağıtım hatasını tek istekle yakalar.

Hangi stratejiyi ne zaman seçmeli

Strateji Kesinti Geri alma hızı Karmaşıklık Uygun olduğu yer
Kademeli (rolling) Yok Orta Düşük Dağıtımların çoğu
Blue-Green Yok Anında Orta Kritik üretim akışları
Canary Yok Hızlı Yüksek Büyük filolar (50+ worker)
Recreate Kısa Yok En düşük Geliştirme ortamları

Kademeli güncelleme varsayılan seçimdir: ek altyapı istemez ve kapasiteyi yalnızca tek worker kadar düşürür. Anında geri alma şartsa Blue-Green desenine geçin — aşağıdaki ilgili rehberlerde tam kurulumu var. Filo büyüdükçe önce küçük bir dilimde deneyen canary modeli mantıklı hâle gelir; 50 worker'ın altında getirdiği karmaşıklık genelde kazandırdığından fazladır.

Saha örneği: İstanbul'da gece dağıtım penceresi

İstanbul merkezli bir e-ticaret ekibinin ödeme adımı QA botlarını düşünün. Bu botlar müşteri trafiğinin en yoğun olduğu 19:00–23:00 arasında satın alma akışını uçtan uca test ediyor ve her turda bir reCAPTCHA v2 doğrulaması çözülüyor. Dağıtım penceresi olarak Europe/Istanbul saatiyle 04:00 seçiliyor: trafik düşük ama sıfır değil, çünkü gece boyunca çalışan regresyon paketleri var.

Bu profilde maxUnavailable=1 ve drain_timeout=120 kombinasyonu rahat çalışır. Sekiz worker'lık bir filo yaklaşık 15–20 dakikada tamamen yenilenir ve hiçbir test turu yarıda kesilmez. Aynı ekip 40 worker'a çıktığında maxUnavailable değerini 4'e yükseltmek toplam süreyi makul tutar; kapasite kaybı ise %10'da kalır.

Bir uyarı: test akışında toplanan veriler kişisel veri içeriyorsa KVKK kapsamındadır. Dağıtım otomasyonunuz log'lara ham form içeriği yazıyorsa, güncelleme penceresi bu log'ların en çok şiştiği andır — sürüm kontrol listenize log maskeleme doğrulamasını da ekleyin.

Sorun giderme

Belirti Sebep Çözüm
Güncelleme sırasında görevler düşüyor Tahliye zaman aşımı çok kısa drain_timeout değerini yükseltin; en uzun çözüm süresini ölçün
Yeni sürümde sağlık kontrolü hep başarısız Sürümde hata ya da eksik ortam değişkeni Geri alın; yeni sürümü önce staging ortamında doğrulayın
Güncelleme çok uzun sürüyor Filo boyutuna göre maxUnavailable çok düşük Büyük filolarda 2–3 worker'a çıkarın
Filoda karışık sürümler kalıyor Geri alma yarıda kesilmiş Güncellenen worker listesini tutun; geri almayı hepsine uygulayın
Worker boş görünüyor ama thread meşgul Aktif görev sayacı finally bloğunda azaltılmıyor Sayacı finally içinde azaltın; istisna yollarında da çalıştığını test edin

SSS

Tahliye süresini hangi ölçüye göre belirlemeliyim?

Filonuzdaki en uzun çözüm süresine bir tampon ekleyin. reCAPTCHA v2 ağırlıklı bir filoda 120 saniyelik drain_timeout rahat çalışır; Turnstile ağırlıklı bir filoda çok daha kısa bir değer yeterlidir. Tahmin etmeyin — worker log'larındaki çözüm sürelerinin 95. yüzdeliğini alın.

Aynı anda kaç worker'ı güncelleyebilirim?

On worker'ın altındaki filolarda teker teker ilerleyin. Daha büyük filolarda toplamın %10–25'i iyi bir aralıktır. %50'yi hiçbir koşulda aşmayın; kalan kapasitenin mevcut istek hacmini karşılaması gerekir.

Sağlık kontrolü olarak bakiye sorgusu yeterli mi?

Hızlı bir ön kontrol olarak evet, tek kontrol olarak hayır. Bakiye sorgusu ağ yolunu ve API anahtarını doğrular ama çözüm yolunu doğrulamaz. Kritik akışlarda her yeni worker'a gerçek bir görev gönderip token'ı alın; yapılandırma hatalarının çoğu tam olarak orada ortaya çıkar.

Geri alma sırasında filoda karışık sürüm kalırsa ne yapmalıyım?

Karışık sürümü tolere etmeyin. Orkestratörünüz güncellenen worker kimliklerini bir listede tutmalı ve geri alma bu listenin tamamı üzerinde çalışmalıdır. Kısmi geri almadan sonra hangi worker'ın hangi davranışı ürettiğini ayırt etmek çok zorlaşır.

Bu desen tek bir CAPTCHA türüne mi bağlı?

Hayır. Orkestratör görev tipinden bağımsız çalışır; değişen tek şey method parametresidir. reCAPTCHA v2, Cloudflare Turnstile ve GeeTest v3 akışlarının hepsi aynı tahliye mantığıyla yönetilir. Tek fark tahliye süresidir: hızlı çözülen türlerde daha kısa bir drain_timeout yeterlidir.

Sonraki adım

Sağlık kontrollü ilk dağıtımınızı bu hafta çalıştırın: CaptchaAI API anahtarınızı alın, orkestratörü staging filonuzda deneyin, sonra üretime taşıyın.

İlgili rehberler:

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