Request pipeline: operation и attempt
Решение
Клиент различает два уровня выполнения:
- operation — один вызов публичного метода SDK с устойчивым
operationId; - attempt — одно фактическое обращение к transport, включая повтор после
5xx,429или успешного обновления авторизации.
Это разделение является контрактом плагинов и ограничителя нагрузки, а не деталью реализации. Operation transformer вызывается один раз и работает с разобранным результатом; attempt interceptor вызывается для каждой сетевой попытки и видит сырой Response.
Порядок выполнения
operation plugins
→ service resolution
→ retry coordinator
→ auth recovery
→ auth preparation
→ queue конечного URL.origin
→ attempt numbering
→ свежие auth headers
→ attempt interceptors и transportПри отключённом rate limiter стадия queue отсутствует. Номер attempt назначается уже после ожидания очереди, поэтому он считает фактические входы в transport, а не запланированные попытки. Повтор исходной операции после 401 получает следующий номер, а сам auth.refresh — вложенная логическая операция с собственным operationId и счётчиком:
users.me: attempt 1 → 401
auth.refresh: attempt 1 → 200
users.me: attempt 2 → 200Инварианты
- Локальный результат operation transformer не входит в очередь и не расходует RPS.
- Backoff выполняется вне queue slot; каждая новая попытка планируется заново.
- Service и разовый
baseUrlразрешаются до выбора очереди. Ключом служит пара «итоговыйURL.origin— серверный бакет», поэтому alias одного origin делят лимитер, а операции из разных бакетов не мешают друг другу. Ключ вычисляется один раз на логическую операцию и не меняется между попытками. - Очередь двухуровневая: задача занимает слот своего бакета, затем общий слот направления. Пауза бакета удерживает её до захвата общего слота, поэтому притормозивший счётчик не занимает общую ёмкость, а суммарная одновременность остаётся равна
concurrency. - Auth preparation не удерживает queue slot. Lazy sign-in и refresh используют тот же pipeline, что и остальные операции.
- Заголовки авторизации читаются после ожидания очереди, непосредственно перед transport.
- Один
401допускает один auth recovery, чтобы свежий, но отклонённый токен не создавал цикл. operationId, HTTP method иRetrySafetyвстроенных операций берутся из единого каталога.auth.signInявно помечен safe для повтора,auth.refreshостаётся unsafe.- После начала
dispose()новые пользовательские операции запрещены. Только внутренние запросы финализации уже накопленной телеметрии получают непубличное разрешение завершиться.
Почему queue находится внутри retry
Если очередь оборачивает весь логический запрос, ожидание backoff занимает concurrency slot, а несколько HTTP-попыток учитываются как один запрос. Кроме того, cache hit начинает зависеть от сетевой нагрузки. Размещение queue на уровне attempt делает число планирований равным числу реальных обращений к transport.
Выбор точки расширения
Используйте operations.use(), если расширение работает со смыслом метода: cache, шифрование, operation-level mocks или преобразование результата. Используйте attempts.use(), если нужны итоговый URL и заголовки, подпись запроса, wire-метрики либо сырой Response каждой попытки.
Подробные типы и примеры находятся в руководстве по плагинам.