Агент-инженер по репозиторию · Модуль 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).

retry.go: экспоненциальный backoff с джиттером и Retry-After (новый файл, фрагмент)
+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?

  • A Ничего, экспоненты достаточно
  • B Джиттер — случайный разброс задержек, чтобы синхронные ретраи не били залпами (thundering herd); и уважение Retry-After
  • C Увеличить размер пула воркеров
  • D Повторять без задержки, но чаще