Агент-инженер по репозиторию · Модуль 6 · Урок 6.2
Обработка 429 и rate-limit: ретраи с backoff
Зачем (какую проблему чиним)
Пул воркеров шлёт параллельные запросы к модели и к forge-API. Рано или поздно прилетит 429 Too Many Requests или overloaded. Наивный мгновенный ретрай только усугубляет шторм. Нужны цивилизованные ретраи с экспоненциальным backoff и уважением заголовка Retry-After.
Решение и альтернативы
Решение: обёртка ретраев вокруг сетевых вызовов: на 429/5xx/overloaded — экспоненциальный backoff с джиттером, уважая Retry-After, с ограничением числа попыток и отменой по контексту. SDK anthropic-sdk-go уже ретраит часть ошибок сам — мы не дублируем его, а добавляем backoff там, где ретраим мы (forge-API), и общий ограничитель параллелизма к модели.
Альтернативы: мгновенный повтор в цикле — DDoS самих себя, бан; фиксированная пауза — хуже экспоненты при длительной перегрузке; без джиттера — синхронные ретраи воркеров бьют залпами (thundering herd). Экспонента + джиттер + Retry-After — стандарт.
DIFF
Добавляем retry с экспоненциальным backoff, джиттером и уважением Retry-After; оборачиваем forge-вызовы.
⚠ Безопасность
Backoff — это и защита: неконтролируемые ретраи могут привести к бану ключа/токена и каскадным сбоям. Ограничитель параллелизма не даёт пулу воркеров превратить всплеск задач в шторм запросов. Отмена по контексту согласована с graceful shutdown: при остановке сервиса ретраи прекращаются.
Проверка
Мок forge, возвращающий 429 с Retry-After: 2, заставляет обёртку выждать и повторить; число попыток ограничено, отмена контекста прерывает ожидание. Тест детерминирован (виртуальное время/мок).
Глубже
Circuit breaker, фолбэки, устойчивость к перегрузке — курс «Продакшн-разработка», Модуль 5 (гл. 10); стоимость/латентность под нагрузкой — Модуль 6 (гл. 12).
+package main
+
+import (
+ "context"
+ "time"
+)
+
+// retry повторяет fn при транзиентных ошибках (429/5xx/overloaded) с
+// экспоненциальным backoff и джиттером; уважает Retry-After; отменяется по ctx.
+func retry(ctx context.Context, max int, fn func() error) error {
+ var err error
+ delay := 500 * time.Millisecond
+ for attempt := 0; attempt < max; attempt++ {
+ if err = fn(); err == nil || !transient(err) {
+ return err
+ }
+ wait := withJitter(delay)
+ if ra, ok := retryAfter(err); ok {
+ wait = ra // сервер сказал, сколько ждать — уважаем
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err() // согласовано с graceful shutdown
+ case <-time.After(wait):
+ }
+ delay *= 2
+ }
+ return err
+}Anti-patterns
| Грабля | Почему плохо | Как правильно |
|---|---|---|
| Мгновенный повтор без задержки | Усиливает перегрузку (DDoS самих себя), ведёт к бану | Экспоненциальный backoff с ограничением попыток |
| Backoff без джиттера | Синхронные ретраи воркеров бьют залпами (thundering herd) | Добавить джиттер, чтобы разнести повторы во времени |
Игнорировать Retry-After | Повтор раньше, чем разрешил сервер, → снова 429 | Уважать Retry-After, когда он есть |
| Ретраить без отмены по контексту | При shutdown ожидание зависает, задачи не завершаются | select на ctx.Done(): отмена прерывает ожидание |
Практическое задание (RA-v5)
- Добавить
retryс экспоненциальным backoff, джиттером и уважениемRetry-After; отмена по контексту. - Обернуть forge-вызовы; добавить ограничитель параллелизма к модели; не дублировать встроенные ретраи SDK.
- Закоммитить:
git commit -m "v5: rate-limit handling with backoff".
Проверка знаний
Несколько воркеров одновременно получили 429 и повторяют запрос. Что важно добавить к экспоненциальному backoff?
Верный ответ: B
B. Без джиттера воркеры, упавшие одновременно, повторят одновременно — снова всплеск. Случайный разброс разносит повторы во времени; Retry-After задаёт минимальную паузу от сервера. Увеличение пула (C) усугубит перегрузку.