Cuando la API de resolución CAPTCHA es lenta o devuelve errores, seguir enviando solicitudes es un desperdicio de tiempo y dinero. El patrón circuit breaker detiene las llamadas a una API dañada, espera la recuperación y se reanuda automáticamente, evitando fallos en cascada en tu pipeline.
Cómo funciona el circuit breaker
Tres estados:
- Cerrado: funcionamiento normal. Las solicitudes pasan. Los fracasos se cuentan.
- Abierto: Demasiados errores. Todas las solicitudes se rechazan inmediatamente sin llamar a la API.
- Medio abierto: después de un período de recuperación, se permite el paso de una solicitud de prueba. Si tiene éxito, el circuito se cierra. Si falla, el circuito se abre nuevamente.
Implementación de Python
import time
import threading
import requests
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
API_KEY = "YOUR_API_KEY"
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = 0
self.state = "closed" # closed, open, half-open
self._lock = threading.Lock()
def call(self, func, *args, **kwargs):
with self._lock:
if self.state == "open":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "half-open"
print("[circuit] State: half-open — testing one request")
else:
remaining = self.recovery_timeout - (
time.time() - self.last_failure_time
)
raise CircuitOpenError(
f"Circuit open — retry in {remaining:.0f}s"
)
try:
result = func(*args, **kwargs)
with self._lock:
self.failure_count = 0
if self.state == "half-open":
print("[circuit] State: closed — API recovered")
self.state = "closed"
return result
except Exception as e:
with self._lock:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
print(
f"[circuit] State: open — "
f"{self.failure_count} failures"
)
raise
class CircuitOpenError(Exception):
pass
def solve_captcha(sitekey, page_url):
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(f"Submit error: {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(f"Poll error: {poll['request']}")
raise TimeoutError(f"Task {task_id} timed out")
# Usage
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30)
for i in range(10):
try:
token = breaker.call(
solve_captcha, "6Le-SITEKEY", "https://example.com"
)
print(f"[task-{i}] Solved: {token[:40]}...")
except CircuitOpenError as e:
print(f"[task-{i}] Skipped: {e}")
except Exception as e:
print(f"[task-{i}] Failed: {e}")
Resultado esperado:
[task-0] Solved: 03AGdBq26ZfPxL...
[task-1] Solved: 03AGdBq27AbCdE...
[task-2] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-3] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-4] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[circuit] State: open — 3 failures
[task-5] Skipped: Circuit open — retry in 28s
[task-6] Skipped: Circuit open — retry in 25s
...
[circuit] State: half-open — testing one request
[task-8] Solved: 03AGdBq28FgHiJ...
[circuit] State: closed — API recovered
Implementación de JavaScript
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 60000;
this.failureCount = 0;
this.lastFailureTime = 0;
this.state = 'closed';
}
async call(fn, ...args) {
if (this.state === 'open') {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = 'half-open';
console.log('[circuit] State: half-open');
} else {
const remaining = this.recoveryTimeout - (Date.now() - this.lastFailureTime);
throw new Error(`Circuit open — retry in ${Math.ceil(remaining / 1000)}s`);
}
}
try {
const result = await fn(...args);
this.failureCount = 0;
if (this.state === 'half-open') {
console.log('[circuit] State: closed — recovered');
}
this.state = 'closed';
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.log(`[circuit] State: open — ${this.failureCount} failures`);
}
throw error;
}
}
}
// Usage
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const breaker = new CircuitBreaker({ failureThreshold: 3, recoveryTimeout: 30000 });
async function solveCaptcha(sitekey, pageurl) {
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('Timeout');
}
(async () => {
for (let i = 0; i < 10; i++) {
try {
const token = await breaker.call(solveCaptcha, '6Le-SITEKEY', 'https://example.com');
console.log(`[task-${i}] Solved: ${token.substring(0, 40)}...`);
} catch (err) {
console.log(`[task-${i}] ${err.message}`);
}
}
})();
Elegir umbrales
| Parámetro | Tráfico bajo (< 10/min) | Alto tráfico (> 100/min) |
|---|---|---|
failure_threshold |
3 | 10 |
recovery_timeout |
30 años | años 60 |
Establezca el umbral de falla lo suficientemente alto como para tolerar errores intermitentes (un solo tiempo de espera no debería activar el circuito) pero lo suficientemente bajo como para dejar de dañar una API defectuosa.
Combinando con lógica de reintento
Usa la lógica de reintento dentro del circuit breaker. El circuit breaker cuenta los fallos finales (una vez agotados los reintentos):
def solve_with_retry(sitekey, page_url, max_retries=2):
for attempt in range(max_retries + 1):
try:
return solve_captcha(sitekey, page_url)
except Exception:
if attempt == max_retries:
raise
time.sleep(2 ** attempt)
# Circuit breaker wraps the retry function
token = breaker.call(solve_with_retry, "6Le-SITEKEY", "https://example.com")
Solución de problemas
| problema | causa | Solución |
|---|---|---|
| El circuito se abre demasiado rápido | Umbral demasiado bajo | Aumentar failure_threshold |
| El circuito nunca se recupera | recovery_timeout demasiado largo |
Reducir a 30-60 segundos |
| Condición de carrera en uso de subprocesos múltiples | Sin bloqueo de estado | Utilice threading.Lock (Python) u operaciones atómicas |
| Todas las solicitudes bloqueadas durante una interrupción parcial | Un solo interruptor para todos los puntos finales | Utilice interruptores separados para los puntos finales de envío y sondeo |
Preguntas frecuentes
¿Debo usar circuit breakers separados para envío y sondeo?
Sí, para sistemas de gran escala. El punto final de envío puede fallar mientras el sondeo aún funciona (o viceversa). Los interruptores separados le brindan un control más preciso.
¿Qué debo hacer cuando el circuito está abierto?
Ponga en cola la tarea CAPTCHA para más adelante, muestre una interfaz de usuario alternativa u omita la operación. VerDegradación elegante cuando la resolución falla.
Crea flujos de trabajo CAPTCHA resilientes con CaptchaAI
Obtén tu clave API en captchaai.com.
Guías relacionadas
- Implementación de lógica de reintento para la API CaptchaAI
- Referencia de códigos de error CaptchaAI
- Degradación elegante cuando la resolución falla