Автоматический выключатель (circuit breaker) защищает от одной ошибки: слать запросы в API решения CAPTCHA, который уже лёг, и терять на этом время и потоки. Если эндпоинт CaptchaAI на несколько минут отвечает ошибками или тайм-аутами, наивный клиент просто множит неудачные попытки по расписанию вместо того, чтобы остановиться и переждать.
Ниже — рабочая реализация на Python и JavaScript, плюс матрица порогов срабатывания, чек-лист мониторинга и разбор типичных проблем: этого достаточно, чтобы подключить выключатель к боевому пайплайну за один вечер.
Три состояния автоматического выключателя
Состояния встроены прямо в код ниже:
- Closed (закрыто) — обычный режим. Запросы идут как обычно, счётчик ошибок обнуляется при каждом успехе.
- Open (открыто) — счётчик ошибок превысил порог. Новые запросы отклоняются мгновенно, без обращения к API — вы не тратите тайм-ауты на заведомо мёртвый эндпоинт.
- Half-open (наполовину открыто) — по истечении
recovery_timeoutпропускается один пробный запрос. Успех — цепь снова закрыта; ошибка — выключатель возвращается вopen, и таймер запускается заново.
Как подобрать пороги срабатывания
| Параметр | Низкий трафик (< 10/мин) | Высокий трафик (> 100/мин) |
|---|---|---|
failure_threshold |
3 | 10 |
recovery_timeout |
30 с | 60 с |
Порог задавайте с запасом на единичные сбои — один случайный тайм-аут не должен опрокидывать цепь, — но не настолько высоким, чтобы выключатель срабатывал только когда API уже лёг. При выборе конкретных цифр учитывайте несколько факторов:
- Профиль трафика. Для низкого трафика (
< 10/мин) даже 2–3 подряд идущих сбоя статистически значимы — держитеfailure_thresholdна уровне 3. Для высокого трафика (> 100/мин) поднимайте порог до 8–10, иначе единичные сетевые сбои будут открывать цепь без причины. - Сетевые условия воркера. Если воркер стоит на хостинге в Европе или Казахстане и сеть до кластера штормит по вечерам,
recovery_timeout30–60 секунд отличает короткий сетевой шторм от реальной деградации сервиса. - Стоимость простоя. Пример из практики: на тарифе BASIC ($15/мес, 5 потоков) час деградации API без выключателя способен занять все 5 потоков бесполезными повторами — именно это выключатель и должен предотвращать.
- Разделение по эндпоинту. Если отправка (
in.php) и опрос (res.php) деградируют независимо, считайте пороги для каждого эндпоинта отдельно — подробнее в разделе «Типичные проблемы» ниже.
Автоматический выключатель на Python
Реализация потокобезопасна: threading.Lock не даёт воркерам ломать состояние друг другу при одновременных вызовах breaker.call().
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
Тот же шаблон для Node.js, только на промисах вместо блокировок: Event Loop гарантирует, что два вызова не меняют состояние параллельно.
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}`);
}
}
})();
Мониторинг и алерты по состоянию выключателя
Логи из примеров выше ([circuit] State: open, half-open, closed) — это не просто вывод в консоль, а полноценный сигнал для мониторинга. В проде эти переходы стоит поднимать в систему логирования и алертинга, а не просто печатать в stdout.
- Логируйте переход в
openотдельным уровнем (warningилиerror) — это первый сигнал, что API решения CAPTCHA деградирует, и повод проверить статус до того, как проблему заметят пользователи. - Считайте долю времени в состоянии
openза сутки или неделю — если она растёт, пороги подобраны неверно или деградация стала систематической, а не эпизодической. - Алертите на серию
half-open → openбез успешного восстановления — два-три таких цикла подряд означают, чтоrecovery_timeoutслишком короткий для реальной длительности сбоя. - Экспортируйте текущее состояние как метрику (
closed=0, open=1, half-open=2) в Prometheus, Grafana или любую систему мониторинга, которую вы уже используете — отдельный дашборд не нужен, одной метрики в существующей панели достаточно.
Когда автоматический выключатель не нужен
Автоматический выключатель — не универсальный ответ на любую нестабильность API. Он окупается, когда объём запросов и цена простоя достаточно велики, чтобы оправдать дополнительную сложность.
- Разовые скрипты и одноразовый импорт. Если код решает десяток CAPTCHA и завершает работу, состояние
open/half-openизбыточно — обычногоtry/exceptс логикой повторов достаточно. - Очень низкий трафик. При паре задач в час разница между «подождать и повторить» и «остановить поток на
recovery_timeout» почти не влияет на суммарное время — выключатель добавляет код, но не экономит потоки. - Единственный воркер без параллелизма. Без нескольких потоков или процессов нет риска, что десятки воркеров одновременно долбят мёртвый эндпоинт — часть пользы выключателя просто не проявляется.
- Когда важнее не потерять ни одной задачи. Выключатель по определению отбрасывает часть запросов в состоянии
open. Если требование — гарантированно поставить каждую задачу в очередь, а не отбросить, комбинируйте его с постепенной деградацией вместо жёсткого отказа.
Типичные проблемы
| Проблема | Причина | Решение |
|---|---|---|
| Цепь размыкается слишком быстро | Порог слишком низкий | Увеличьте failure_threshold |
| Цепь никогда не восстанавливается | recovery_timeout слишком длинный |
Уменьшите до 30–60 секунд |
| Гонка состояний при многопоточном использовании | Нет блокировки состояния | Используйте threading.Lock (Python) или атомарные операции |
| Все запросы блокируются при частичном сбое | Один выключатель на все эндпоинты | Заведите отдельные выключатели для отправки и опроса |
Быстрая проверка перед тем, как менять настройки:
- Смотрите на
failure_countв логах перед тем, как трогать пороги — часто дело не в цифрах, а в том, что срабатывания считаются не там (общий выключатель на оба эндпоинта, см. последнюю строку таблицы). - Проверьте, не совпадает ли скачок ошибок с деплоем или ротацией API-ключа — это внешняя причина, а не деградация API, и circuit breaker тут не поможет.
- Если цепь колеблется между
openиhalf-openкаждые 30–60 секунд,recovery_timeoutпочти наверняка короче реальной длительности сбоя — увеличьте его прежде, чем трогатьfailure_threshold.
Совмещаем с логикой повторов
Логику повторов держите внутри выключателя. Тогда он считает не любую ошибку, а только окончательный отказ — тот, что пережил все попытки solve_with_retry:
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")
Часто задаваемые вопросы
Нужны ли отдельные выключатели для отправки задачи и опроса результата?
Да, если нагрузка выше пары сотен задач в час. Эндпоинт отправки (in.php) и эндпоинт опроса (res.php) могут деградировать независимо — отдельные выключатели дают более точный контроль и не блокируют опрос уже отправленных задач, если сломался только приём новых.
Что делать, пока цепь разомкнута?
Поставить задачу в очередь, показать запасной UI или пропустить шаг — выбор зависит от сценария. Подробнее — в статье про постепенную деградацию при сбоях решения.
Как выбрать recovery_timeout, чтобы не терять реальные заявки?
Начните с 30–60 секунд и сверьтесь с логами: если сбои длятся 10–15 секунд, долгий тайм-аут задерживает возврат трафика; если сбои растягиваются на минуты, короткий тайм-аут заставит выключатель дёргаться между open и half-open.
Можно ли использовать один экземпляр CircuitBreaker сразу на несколько потоков?
Да — для этого в примере на Python нужен threading.Lock: без него два потока могут посчитать разный failure_count. В Node.js отдельная блокировка не нужна — за это отвечает Event Loop.
Что будет, если API восстановится раньше, чем истечёт recovery_timeout?
Ничего страшного — до истечения таймера новые запросы просто отклоняются исключением, не доходя до API. Как только recovery_timeout истечёт, выключатель перейдёт в half-open и пропустит один пробный запрос; если API уже восстановился, он тут же это подтвердит и вернётся в closed. Цена — несколько лишних секунд ожидания, а не потерянные задачи.
Постройте отказоустойчивый пайплайн решения CAPTCHA с CaptchaAI
Получите API-ключ на captchaai.com и добавьте автоматический выключатель поверх своих потоков — без переписывания остальной интеграции.