Поэтому я написал одну, которая объединяет всё.

Когда простой API-клиент превращается в зоопарк

Любой проект начинается с чего-то такого:

import httpx

async def fetch_user(user_id: str):
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://api.example.com/users/{user_id}")
        return r.json()

А потом реальность:

  • API лимитирует запросы

  • иногда отвечает 503

  • иногда виснет

  • иногда падает полностью

  • и retry может убить уже ваш сервис

И начинается подключение библиотек:

  • rate limit

  • retry

  • circuit breaker

И функция превращается в это:

@breaker
@retry(...)
@sleep_and_retry
@limits(...)
async def fetch_user(...):
    ...

Формально всё работает.

Но появляется ощущение, что ты не пишешь бизнес-логику.

Ты пишешь инфраструктурный клей.


Главная проблема — не в библиотеках

Каждая библиотека сама по себе нормальная.

Проблема в композиции:

  • разные модели времени

  • разные абстракции ошибок

  • разные API

  • сложное тестирование

  • непредсказуемое поведение при нагрузке

Retry + limiter + breaker — это уже система.

А мы собираем систему из несвязанных кусочков.


Идея: сделать устойчивость конвейером

Вместо “башни декораторов” я хотел одно:

единый pipeline выполнения вызова

Чтобы каждый запрос проходил через понятную схему:

Circuit breaker
→ Rate limiter
→ Retry loop
→ Запись результата

Без магии.
Без хаоса.
Без glue-кода.

Получилось вот так:

from limitpal import (
    AsyncResilientExecutor,
    AsyncTokenBucket,
    RetryPolicy,
    CircuitBreaker
)

executor = AsyncResilientExecutor(
    limiter=AsyncTokenBucket(capacity=10, refill_rate=100/60),
    retry_policy=RetryPolicy(max_attempts=3),
    circuit_breaker=CircuitBreaker(failure_threshold=5)
)

result = await executor.run("user:123", api_call)

Одна точка входа.
Одна модель поведения.
Одна архитектура.


Почему это важно

Retry без limiter — опасен.
Limiter без breaker — слеп.
Breaker без retry — жёсткий.

Только вместе они образуют стратегию устойчивости.

Именно стратегия, а не набор фич.


Тестирование — скрытая боль

Самая неприятная часть таких систем — время.

Традиционно:

time.sleep(1)

Медленно.
Флейково.
Непредсказуемо.

Поэтому внутри используется управляемые часы:

clock.advance(1.0)

Тесты выполняются мгновенно, но поведение идентично реальному.

Это звучит как мелочь.

На практике — спасение для CI и нервов.


Что в итоге получилось

Я собрал всё в одну библиотеку — LimitPal.

Это не “ещё один rate limiter”.

Это попытка сделать устойчивость архитектурным примитивом:

  • rate limiting

  • retry с backoff и jitter

  • circuit breaker

  • композиция стратегий

  • sync + async API

  • детерминированные тесты

Без внешних зависимостей.


Когда это имеет смысл

Если вы:

  • пишете API-клиенты

  • ходите во внешние сервисы

  • делаете фоновые джобы

  • боитесь retry-штормов

  • хотите предсказуемое поведение под нагрузкой

тогда архитектурный подход окупается.

Если нужен только retry — можно взять маленькую либу.

Но как только появляется композиция — начинается системная инженерия.


Документация:
https://limitpal.readthedocs.io/

Репозиторий:
https://github.com/Guli-vali/limitpal

Если идея зашл — буду рад обратной связи.