Когда API решения CAPTCHA работает медленно или возвращает ошибки, продолжение отправки запросов приводит к потере времени и денег. Шаблон автоматического выключателя прекращает вызов отказавшего API, ожидает восстановления и автоматически возобновляет работу, предотвращая каскадные сбои в вашем конвейере.
Как работают автоматические выключатели
Три государства:
- Закрыто — Нормальная работа. Запросы выполняются. Ошибки подсчитываются.
- Открыть — слишком много ошибок. Все запросы немедленно отклоняются без вызова API.
- Полуоткрытый — после периода восстановления разрешается пройти один тестовый запрос. В случае успеха цепь замыкается. В случае неудачи цепь размыкается снова.
Реализация 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}")
Ожидаемый результат:
[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
Реализация 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}`);
}
}
})();
Выбор порогов
| Параметр | Низкий трафик (< 10/min) | Высокий трафик (> 100/min) |
|---|---|---|
failure_threshold |
3 | 10 |
recovery_timeout |
30 с | 60-е годы |
Установите порог сбоя достаточно высоким, чтобы допускать периодические ошибки (единственный тайм-аут не должен отключать схему), но достаточно низким, чтобы не допустить сбоев в работе API.
Объединение с логикой повтора
Используйте логику повтора внутри автоматического выключателя. Автоматический выключатель подсчитывает окончательные отказы (после того, как попытки исчерпаны):
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")
Поиск неисправностей
| Проблема | Причина | Исправить |
|---|---|---|
| Цепь размыкается слишком быстро | Порог слишком низкий | Увеличить failure_threshold |
| Схема никогда не восстанавливается | recovery_timeout слишком длинный |
Уменьшите до 30-60 секунд. |
| Состояние гонки при многопоточном использовании | Нет блокировки состояния | Используйте threading.Lock (Python) или атомарные операции. |
| Все запросы заблокированы во время частичного отключения | Один прерыватель для всех конечных точек | Используйте отдельные прерыватели для конечных точек отправки и опроса. |
Часто задаваемые вопросы
Должен ли я использовать отдельные автоматические выключатели для отправки и опроса?
Да, для крупномасштабных систем. Конечная точка отправки может выйти из строя, пока опрос все еще работает (или наоборот). Отдельные выключатели обеспечивают более точный контроль.
Что делать, если цепь разомкнута?
Поставьте задачу CAPTCHA в очередь на будущее, покажите резервный пользовательский интерфейс или пропустите операцию. ВидетьГрациозная деградация при неудачном решении.
Создавайте устойчивые рабочие процессы CAPTCHA с помощью CaptchaAI
Получите ключ API по адресуcaptchaai.com.
Связанные руководства
- Реализация логики повтора для API CaptchaAI
- Справочник кодов ошибок CaptchaAI
- Грациозная деградация при неудачном решении