Перейти к содержанию

Transport и retry

Transport — единственный слой, который работает с httpx, таймаутами, retry и mapping HTTP-ошибок. Домены и OperationExecutor не повторяют эту логику, иначе публичное поведение разных разделов начало бы расходиться.

flowchart TD
    call[OperationExecutor request] --> auth{Нужен токен?}
    auth -- да --> token[AuthProvider]
    auth -- нет --> send[httpx request]
    token --> send
    send --> status{Ответ}
    status -- 2xx --> map[JSON, empty или binary response]
    status -- 401 --> refresh[Инвалидация токена]
    refresh --> retry401{Можно повторить?}
    retry401 -- да --> token
    retry401 -- нет --> error[Typed exception]
    status -- 429/5xx --> retry{Retry допустим?}
    retry -- да --> wait[Backoff / Retry-After]
    wait --> send
    retry -- нет --> error
    status -- 4xx --> error

Что повторяется

Retry применяется только там, где операция помечена как безопасная для повтора. Read/list/probe операции обычно допускают retry. Write-операции получают retry только при явной идемпотентности, например через idempotency_key, или когда конкретный OperationSpec помечает операцию как безопасную.

POST и PATCH без idempotency_key не повторяются даже при retryable transport-политике. DELETE тоже не повторяется без idempotency_key, если операция явно не помечена как безопасная для retry через OperationSpec. Это правило имеет приоритет над RetryPolicy.retryable_methods, поэтому глобальная политика не может случайно включить повтор небезопасного удаления.

429 учитывает Retry-After, если upstream его вернул. Если Retry-After отсутствует, transport использует обычный exponential backoff с jitter. Для 5xx используется retry-политика transport-слоя. Ошибки маппинга не повторяются: если JSON уже получен, но не соответствует контракту модели, это ResponseMappingError, а не сетевой сбой.

Чтобы снижать вероятность 429 до ответа upstream, можно включить локальный token bucket через AVITO_RATE_LIMIT_ENABLED=true. Лимитер применяется в transport-слое перед отправкой запроса и дополнительно учитывает X-RateLimit-Remaining: 0, когда API возвращает этот заголовок.

Логирование transport

Каждая HTTP-попытка пишет debug-событие в logger avito.transport с полями operation, endpoint, method, attempt, status, latency_ms и request_id. Для сетевых ошибок до ответа upstream status и request_id остаются None, но попытка всё равно логируется. Retry-события используют те же operation, endpoint, method, attempt и status, а также добавляют delay_ms и reason.

Transport не логирует body, query payload, OAuth headers, idempotency keys и другие секретные значения. Для подробностей об ошибке используйте поля типизированного исключения.

Почему retry не в доменах

Доменный объект должен описывать публичный сценарий: order_label().create() или ad_stats().get_item_stats(). Если retry появится на этом уровне, одинаковые HTTP-коды начнут вести себя по-разному в разных пакетах. Поэтому retry централизован и проверяется через transport/fake transport.

Подробные исключения смотрите в модели ошибок и reference по исключениям.