Lorsque l'API de résolution de CAPTCHA est lente ou renvoie des erreurs, continuer à envoyer des requêtes fait perdre du temps et de l'argent. Le modèle de disjoncteur arrête d'appeler une API défaillante, attend la récupération et reprend automatiquement, évitant ainsi les pannes en cascade dans votre pipeline.
Comment fonctionnent les disjoncteurs
Trois états :
- Fermé – Fonctionnement normal. Les demandes transitent. Les échecs sont comptés.
- Ouvert – Trop d'échecs. Toutes les demandes sont rejetées immédiatement sans appeler l'API.
- Semi-ouvert – Après une période de refroidissement, une demande de test est autorisée. Si cela réussit, le circuit se ferme. En cas d'échec, le circuit s'ouvre à nouveau.
Implémentation 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}")
Résultat attendu :
[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
Implémentation 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}`);
}
}
})();
Choisir des seuils
| Paramètre | Faible trafic (< 10/min) | Trafic élevé (> 100/min) |
|---|---|---|
failure_threshold |
3 | 10 |
recovery_timeout |
30s | 60s |
Définissez le seuil de défaillance suffisamment haut pour tolérer des erreurs intermittentes (un seul délai d'attente ne devrait pas déclencher le circuit) mais suffisamment bas pour arrêter de marteler une API défaillante.
Combinaison avec la logique de nouvelle tentative
Utilisez la logique de nouvelle tentative à l'intérieur du disjoncteur. Le disjoncteur compte les pannes finales (une fois les tentatives épuisées) :
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")
Dépannage
| Problème | Parce que | Corriger |
|---|---|---|
| Le circuit s'ouvre trop vite | Seuil trop bas | Augmenter failure_threshold |
| Le circuit ne récupère jamais | recovery_timeout trop long |
Réduire à 30-60 secondes |
| Condition de concurrence en utilisation multithread | Pas de verrouillage sur l'état | Utilisez threading.Lock (Python) ou des opérations atomiques |
| Toutes les demandes bloquées lors d'une panne partielle | Disjoncteur unique pour tous les points finaux | Utiliser des disjoncteurs distincts pour les points de terminaison de soumission et d'interrogation |
FAQ
Dois-je utiliser des disjoncteurs séparés pour la soumission et le sondage ?
Oui, pour les systèmes à grande échelle. Le point de terminaison de soumission peut échouer alors que l'interrogation fonctionne toujours (ou vice versa). Des disjoncteurs séparés vous offrent un contrôle plus fin.
Que dois-je faire lorsque le circuit est ouvert ?
Mettez la tâche CAPTCHA en file d'attente pour plus tard, affichez une interface utilisateur de secours ou ignorez l'opération. VoirDégradation gracieuse en cas d'échec de la résolution.
Créez des flux de travail CAPTCHA résilients avec CaptchaAI
Obtenez votre clé API surcaptchaai.com.
Guides associés
- Implémentation d'une logique de nouvelle tentative pour l'API CaptchaAI
- Référence des codes d'erreur CaptchaAI
- Dégradation gracieuse en cas d'échec de la résolution