API Tutorials

Шаблон автоматического выключателя для вызовов API CAPTCHA

Автоматический выключатель (circuit breaker) защищает от одной ошибки: слать запросы в API решения CAPTCHA, который уже лёг, и терять на этом время и потоки. Если эндпоинт CaptchaAI на несколько минут отвечает ошибками или тайм-аутами, наивный клиент просто множит неудачные попытки по расписанию вместо того, чтобы остановиться и переждать.

Ниже — рабочая реализация на Python и JavaScript, плюс матрица порогов срабатывания, чек-лист мониторинга и разбор типичных проблем: этого достаточно, чтобы подключить выключатель к боевому пайплайну за один вечер.


Три состояния автоматического выключателя

Состояния встроены прямо в код ниже:

  1. Closed (закрыто) — обычный режим. Запросы идут как обычно, счётчик ошибок обнуляется при каждом успехе.
  2. Open (открыто) — счётчик ошибок превысил порог. Новые запросы отклоняются мгновенно, без обращения к API — вы не тратите тайм-ауты на заведомо мёртвый эндпоинт.
  3. 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_timeout 30–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 и добавьте автоматический выключатель поверх своих потоков — без переписывания остальной интеграции.


Похожие руководства

Комментарии для этой статьи отключены.