Kısa cevap: birden fazla worker aynı hedef site için CAPTCHA çözüyorsa, oturum durumunu her worker'ın belleğinde değil Redis gibi paylaşılan bir depoda tutun. Çerezler, token'lar ve proxy atamaları merkezde durunca hedef site tek ve tutarlı bir oturum görür, worker'lar birbirinin çözümünü yeniden kullanır.
Bu tutarlılık, İstanbul'daki bir e-ticaret ekibinin ödeme akışını paralel worker'larla test ettiği bir QA senaryosunda hemen fark edilir: durum paylaşılmazsa her worker ayrı çerez ve ayrı IP taşır, hedef site bunu düzensiz davranış olarak okur.
Sorun: her worker kendi oturumunu taşır
Paylaşılan durum olmadan her worker sıfırdan başlar: farklı çerezler, farklı IP'ler ve farklı tarayıcı profilleri üretir, süresi dolmuş oturumlardaki çözümleri boşa harcar.
Worker 1 → Login → Solve CAPTCHA → Get cookie A → Submit form ✅
Worker 2 → New session → Solve CAPTCHA → Get cookie B → Submit form ✅
Worker 3 → Reuse cookie A? → Cookie expired → Solve CAPTCHA → Fail ❌
Üçüncü worker çürümüş bir çereze güvendiği için CAPTCHA'yı yeniden çözmek zorunda kalır; bu tekrarlar gecikmeyi ve maliyeti artırır.
Paylaşılan durumda ne saklanır?
Her bileşenin ömrü farklıdır, paylaşım stratejisi de buna göre değişir: kısa ömürlü veriler kısa TTL'li anahtarlarda, kimlik doğrulama verileri TTL'li hash'lerde durur.
| Durum Bileşeni | Ömür boyu | Paylaşım Stratejisi |
|---|---|---|
| Kimlik doğrulama çerezleri | Dakikalardan saatlere | TTL ile Redis |
| CAPTCHA belirteçleri | 90–300 saniye | Redis listesi (kısa TTL) |
qa_session_cookie çerezleri |
~30 dakika | Redis karma |
| CSRF belirteçleri | Sayfa yükü başına | Paylaşmayın; her çalışanın kendine ait bir hakkı var |
| Tarayıcı test profil yapılandırması | Kalıcı | Çalışma zamanı durumu değil yapılandırma |
| Vekil ataması | Oturum başına | Redis destekli proxy havuzu |
CSRF token'larını asla paylaşmayın: her worker kendi token'ını almalıdır.
Mimari: Redis merkezli durum deposu
Tüm worker'lar aynı Redis örneğine bakar: çerezler domain'e, token'lar sitekey'e, proxy'ler bir havuza, kilitler domain başına anahtarlanır.
┌──────────────────────────────────────┐
│ Session State Store │
│ (Redis) │
│ │
│ cookies:{domain} → Hash │
│ tokens:{sitekey} → List │
│ proxies:pool → Set │
│ locks:{domain}:{worker} → String │
└─────┬──────────┬──────────┬──────────┘
│ │ │
┌───▼───┐ ┌──▼────┐ ┌──▼────┐
│Worker1│ │Worker2│ │Worker3│
└───────┘ └───────┘ └───────┘
Paylaşılan durumu yetkili QA akışıyla sınırlayın
Paylaşılan oturum durumunu yalnızca kendi test ortamlarınızda ya da yetki verilmiş QA akışlarında kullanın. İstanbul'daki ekip örneğinde olduğu gibi, çerez ve oturum verisi kişisel veri içerebilir; Türkiye'de bu tür senaryolar KVKK yükümlülükleri kapsamındadır. Redis'te ne sakladığınızı ve ne kadar süre tuttuğunuzu bilinçli seçin:
- Çerezleri yalnızca test süresi boyunca saklayın; kısa TTL ile otomatik silinmelerini sağlayın.
- Gerçek kullanıcı oturumları yerine
staging.example.com/qa-logingibi QA hesaplarını kullanın. - Redis örneğini erişim denetimiyle koruyun, herkese açık bırakmayın.
Bu üç kural, paylaşılan durumu hem güvenli hem de denetlenebilir tutar.
Python ile uygulama
Oturum deposu (SessionStore)
SessionStore çerezleri ve token'ları TTL ile saklar, ayrıca oturumu tek worker'ın yönetmesi için bir kilit sunar. save_token kısa TTL kullanır çünkü token'lar hızlı çürür.
import os
import json
import time
import redis
import requests
from datetime import datetime, timezone
r = redis.Redis(
host=os.environ.get("REDIS_HOST", "localhost"),
port=int(os.environ.get("REDIS_PORT", 6379)),
decode_responses=True
)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
class SessionStore:
"""Shared session state across distributed workers."""
def __init__(self, domain):
self.domain = domain
self.cookie_key = f"session:cookies:{domain}"
self.token_key = f"session:tokens:{domain}"
def save_cookies(self, cookies, ttl=1800):
"""Store cookies from a successful session."""
cookie_data = {name: value for name, value in cookies.items()}
r.hset(self.cookie_key, mapping=cookie_data)
r.expire(self.cookie_key, ttl)
def get_cookies(self):
"""Retrieve shared cookies."""
cookies = r.hgetall(self.cookie_key)
return cookies if cookies else None
def save_token(self, sitekey, token, ttl=80):
"""Store a solved CAPTCHA token."""
key = f"{self.token_key}:{sitekey}"
r.rpush(key, token)
r.expire(key, ttl)
def get_token(self, sitekey):
"""Pop a cached CAPTCHA token."""
key = f"{self.token_key}:{sitekey}"
return r.lpop(key)
def acquire_session_lock(self, worker_id, ttl=300):
"""Ensure only one worker manages the session at a time."""
lock_key = f"session:lock:{self.domain}"
return r.set(lock_key, worker_id, nx=True, ex=ttl)
def release_session_lock(self, worker_id):
"""Release session lock if this worker holds it."""
lock_key = f"session:lock:{self.domain}"
current = r.get(lock_key)
if current == worker_id:
r.delete(lock_key)
Paylaşılan durumu kullanan worker
Worker önce paylaşılan çerezleri yükler, sonra CAPTCHA'yı çözer. solve_captcha her seferinde CaptchaAI'ye gitmez: önce Redis'teki token havuzuna bakar. Elde ettiği token'ı g-recaptcha-response alanına enjekte edip formu gönderir.
class CaptchaWorker:
def __init__(self, worker_id, domain):
self.worker_id = worker_id
self.store = SessionStore(domain)
self.session = requests.Session()
def setup_session(self):
"""Load shared cookies into this worker's session."""
cookies = self.store.get_cookies()
if cookies:
for name, value in cookies.items():
self.session.cookies.set(name, value)
return True
return False
def solve_captcha(self, sitekey, pageurl):
"""Solve with token cache and session sharing."""
# Check for cached token
cached = self.store.get_token(sitekey)
if cached:
return {"solution": cached, "source": "cache"}
# Solve via CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": 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 = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
token = result["request"]
self.store.save_token(sitekey, token)
return {"solution": token, "source": "api"}
if result.get("request") != "CAPCHA_NOT_READY":
return {"error": result.get("request")}
return {"error": "TIMEOUT"}
def process_page(self, url, sitekey):
"""Full workflow: setup session → solve CAPTCHA → submit."""
# Load shared session
self.setup_session()
# Solve CAPTCHA
result = self.solve_captcha(sitekey, url)
if "error" in result:
return result
# Submit form with token
response = self.session.post(url, data={
"g-recaptcha-response": result["solution"]
})
# Share resulting cookies
self.store.save_cookies(dict(self.session.cookies))
return {"status": response.status_code, "source": result["source"]}
Proxy havuzu yönetimi
Tüm worker'lar aynı IP'den çıkarsa hedef site onları tek varlık sayar. ProxyPool her worker'a havuzdan boş bir proxy atar, iş bitince geri verir.
class ProxyPool:
"""Distribute proxies across workers to avoid IP conflicts."""
def __init__(self, proxies):
self.pool_key = "session:proxy_pool"
self.assigned_key = "session:proxy_assigned"
# Initialize pool
for proxy in proxies:
r.sadd(self.pool_key, proxy)
def acquire_proxy(self, worker_id, ttl=600):
"""Assign an unused proxy to a worker."""
# Check if worker already has one
existing = r.hget(self.assigned_key, worker_id)
if existing:
return existing
# Pop from available pool
proxy = r.spop(self.pool_key)
if proxy:
r.hset(self.assigned_key, worker_id, proxy)
r.expire(self.assigned_key, ttl)
return proxy
return None
def release_proxy(self, worker_id):
"""Return proxy to the pool."""
proxy = r.hget(self.assigned_key, worker_id)
if proxy:
r.sadd(self.pool_key, proxy)
r.hdel(self.assigned_key, worker_id)
Node.js ile uygulama
Aynı deseni Node.js tarafında ioredis ve axios ile kurabilirsiniz. workerSolve önce havuza bakar, boşsa CaptchaAI'ye gönderip sonucu sorgular; Turnstile ya da reCAPTCHA fark etmeksizin akış aynıdır.
const Redis = require("ioredis");
const axios = require("axios");
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
class SessionStore {
constructor(domain) {
this.domain = domain;
this.cookieKey = `session:cookies:${domain}`;
this.tokenKey = `session:tokens:${domain}`;
}
async saveCookies(cookies, ttl = 1800) {
const entries = Object.entries(cookies).flat();
if (entries.length > 0) {
await redis.hset(this.cookieKey, ...entries);
await redis.expire(this.cookieKey, ttl);
}
}
async getCookies() {
return await redis.hgetall(this.cookieKey);
}
async saveToken(sitekey, token, ttl = 80) {
const key = `${this.tokenKey}:${sitekey}`;
await redis.rpush(key, token);
await redis.expire(key, ttl);
}
async getToken(sitekey) {
return await redis.lpop(`${this.tokenKey}:${sitekey}`);
}
async acquireLock(workerId, ttl = 300) {
const result = await redis.set(`session:lock:${this.domain}`, workerId, "NX", "EX", ttl);
return result === "OK";
}
async releaseLock(workerId) {
const current = await redis.get(`session:lock:${this.domain}`);
if (current === workerId) await redis.del(`session:lock:${this.domain}`);
}
}
async function workerSolve(store, sitekey, pageurl) {
const cached = await store.getToken(sitekey);
if (cached) return { solution: cached, source: "cache" };
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) return { error: submit.data.request };
const captchaId = submit.data.request;
for (let i = 0; i < 60; 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: captchaId, json: 1 },
});
if (poll.data.status === 1) {
await store.saveToken(sitekey, poll.data.request);
return { solution: poll.data.request, source: "api" };
}
if (poll.data.request !== "CAPCHA_NOT_READY") return { error: poll.data.request };
}
return { error: "TIMEOUT" };
}
Durum yönetimi desenleri
Desenleri birleştirebilirsiniz: kimlik doğrulamalı akışlarda oturum kilidi ve çerez paylaşımı, durumsuz yüksek verimli çözümlerde token havuzu öne çıkar.
| Desen | Ne Zaman Kullanılmalı |
|---|---|
| Oturum kilidi | Bir çalışan oturum açma işlemini yönetir, diğerleri ise çerezleri kullanır |
| Jeton havuzu | Yüksek verimlilik: belirteçleri önceden çözün ve dağıtın |
| Çerez paylaşımı | Çalışanların kimliği doğrulanmış oturumlara ihtiyacı var |
| Vekil yakınlığı | Hedef site IP oturumu bağlamayı izler |
Sorun giderme
Dağıtık kurulumlarda sorunların çoğu eksik paylaşımdan ya da yanlış TTL'den doğar.
| Sorun | Sebep | Düzeltme |
|---|---|---|
| Çalışanlar farklı oturumlar alıyor | Redis aracılığıyla paylaşılmayan çerezler | Başarılı isteklerden sonra save_cookies'nin çağrıldığını doğrulayın |
| Tokenın süresi, diğer çalışanlar onu kullanmadan önce doldu | TTL çok uzun veya ağ gecikmesi | TTL güvenlik marjını azaltın; Jetonları alındıktan sonraki 10 saniye içinde kullanın |
| Oturum kilidi hiç serbest kalmadı | Worker çöktü | Kilit anahtarındaki TTL otomatik serbest bırakır (varsayılan 300 saniye) |
| Hedef site çalışanları engelliyor | Tüm çalışanlar aynı proxy'yi kullanıyor | Çalışan başına benzeşimle proxy havuzunu kullanın |
Sık sorulan sorular
CaptchaAI thread sayısı worker sayısıyla eşleşmeli mi?
Zorunlu değil ama pratik bir tavan koyar. CaptchaAI thread bazlı faturalandırır; her thread aynı anda çözülen bir CAPTCHA'dır. BASIC ($15/ay, 5 thread) ile beş çözüm akışını paralel yürütebilirsiniz; daha fazla eşzamanlı worker için ADVANCE ($90/ay, 50 thread) gibi bir plana geçin.
Birden fazla worker token havuzunu aynı anda çekerse çakışma olur mu?
Hayır. Token'lar Redis listesinde tutulur ve lpop atomiktir; iki worker aynı token'ı asla alamaz. Havuz boşsa worker CaptchaAI'ye yeni istek gönderip çözer ve sonucu listeye ekler.
Redis yerine başka bir durum deposu kullanabilir miyim?
Evet. Desen depoya bağlı değil; atomik işlemler ve TTL sunan herhangi bir paylaşımlı depo işe yarar. Redis düşük gecikmesi ve yerleşik TTL'si nedeniyle pratik seçimdir.
Bir worker çökerse paylaşılan oturuma ne olur?
Oturum kilidi TTL ile korunur (varsayılan 300 saniye), böylece çöken worker kilidi sonsuza kadar tutamaz; süre dolunca başka bir worker oturumu devralır. Paylaşılan çerezler Redis'te kaldığı için tek bir worker'ın çökmesi diğerlerini durdurmaz.
Sonraki adımlar
Dağıtık CAPTCHA worker'larınızı tutarlı oturumlarla koordine edin: CaptchaAI API anahtarınızı alın ve reCAPTCHA ile Turnstile'ı aynı akıştan çözün.
İlgili kılavuzlar: