Проблема видна как лавина: один upstream отвечает 503 или 429, а несколько клиентов одновременно повторяют запрос. Цена — усилить перегрузку именно в момент восстановления, увеличить latency и получить каскад отказов. Без различия между безопасным чтением и записью retry превращается в генератор дублей.
Причина — считать повтор одной настройкой «три попытки». Правильное решение зависит от HTTP-метода, кода ответа, идемпотентности операции, Retry-After, текущего deadline и нагрузки. Экспоненциальная задержка уменьшает частоту, а jitter раздвигает одинаковые моменты старта; ни одна из них не делает небезопасную запись безопасной.
Сначала право на повтор
GET обычно проектируют как идемпотентное чтение, но серверная реализация и внешние побочные эффекты всё равно требуют проверки. POST может создать ресурс повторно. Для такой операции нужен idempotency key и серверное хранение результата, либо клиент должен получить способ запросить состояние операции. Timeout означает неизвестный результат, а не подтверждённый отказ.
Статус 429 сообщает о частоте запросов, но не выбирает за клиента точный алгоритм. 503 может означать временную недоступность, но повтор с коротким интервалом усугубит проблему. 400 обычно не меняется от повтора. Политика должна принимать method, status, наличие Retry-After и остаток deadline, а не только boolean «network error».
| Сигнал | Повтор | Задержка | Риск |
|---|---|---|---|
| 429 + Retry-After | только если операция допустима | не раньше указанного времени | клиенты проснутся одновременно |
| 503 без Retry-After | ограниченно | backoff + jitter | перегрузить восстанавливающийся upstream |
| timeout GET | возможен | остаток deadline | ответ мог быть готов на сервере |
| timeout POST | только с ключом/проверкой | короткий controlled retry | дублирование записи |
| 400/401/403 | нет | не нужна | повтор маскирует дефект входа или права |
Экспонента не должна расти бесконечно
Базовая формула min(cap, base × 2^attempt) ограничивает задержку сверху. Jitter добавляет случайное смещение, чтобы тысячи клиентов не повторили в одну миллисекунду. Но общий deadline должен иметь приоритет: если до его конца осталось 40 мс, не имеет смысла ждать 500 мс ради следующей попытки. Операция завершается, а причина сохраняется.
Случайность нужно добавлять так, чтобы наблюдение оставалось возможным. Логируйте номер попытки, рассчитанную задержку, статус и остаток deadline, но не секреты и полное тело запроса. Для тестов используйте переданный генератор случайных чисел или фиксированный jitter. Иначе тест будет иногда падать, а реальная политика останется непроверяемой.
Runnable-пример: рассчитать ограниченную задержку
Функция получает номер попытки, базовую задержку, cap и учебный jitter. Она возвращает экспоненциальную часть и итог. В реальном клиенте jitter обычно генерируется отдельно и учитывается в deadline; здесь он передан числом, чтобы результат был воспроизводимым. На третьей попытке с base 100 и cap 1000 экспонента равна 800.
import { calculateRetryDelay } from './upgrade-2027-11.mjs';
const third = calculateRetryDelay({
attempt: 3,
baseMs: 100,
capMs: 1000,
jitterMs: 37,
});
const invalid = calculateRetryDelay({
attempt: -1,
baseMs: 100,
capMs: 1000,
});
console.log(third.ok, third.exponentialMs, third.delayMs);
console.log(invalid.ok, invalid.reason);
// true 800 837
// false retry-input-invalidПорядок настройки retry
- Для каждого метода выпишите побочные эффекты и способ узнать результат после timeout. Без этого число попыток не имеет смысла.
- Составьте таблицу статусов: что можно повторять, какой сигнал приходит от сервера и когда нужно остановиться.
- Передавайте абсолютный deadline или остаток времени во все попытки. Не выдавайте каждой попытке новый полный бюджет.
- Задайте base, cap и предел попыток, затем добавьте jitter. Проверьте формулу на нулевой, первой и предельной попытке.
- Обработайте Retry-After как верхнюю границу политики сервера, но всё равно сравните её с deadline.
- Нагрузочным тестом проверьте восстановление upstream: retry не должен создавать вторую волну запросов быстрее исходной.
Идемпотентный ключ — это не request-id
Request-id помогает найти попытку в логах, но сам по себе не говорит серверу, что две попытки означают одну операцию. Idempotency key должен быть связан с семантикой команды, сроком хранения и результатом. Сервер обязан решить, что вернуть при повторе с тем же ключом и другим телом. Это часть контракта, а не случайное поле заголовка.
Даже при ключе остаются границы: сбой между записью и сохранением результата, истечение TTL, разные пользователи и смена версии схемы. Поэтому ключ не отменяет тесты повторной доставки и проверку состояния. Он даёт серверу возможность дедуплицировать операцию, но не обещает успешный outcome.
Ограничения и следующий шаг
Расчёт задержки не реализует случайный генератор, circuit breaker, rate limit и очередь. RFC 6585 описывает статус 429, но не определяет вашу политику. Примеры чисел учебные и не подходят для копирования без измерения downstream и общего deadline.
Следующий шаг — выбрать один endpoint, описать повтор для каждого метода и прогнать искусственный 429/503 с фиксированным временем. Отдельно проверьте timeout POST: повтор должен либо использовать idempotency key, либо перейти к запросу статуса, а не автоматически создать вторую запись.
Проверяемые источники
- RFC 9110 — HTTP Semantics — IETF, июнь 2022 года, RFC 9110, Standards Track. Применение: Разделяет свойства методов и идемпотентность, необходимые для решения, допустим ли повтор запроса. Граница: Не задаёт политику retry конкретного клиента, backoff и максимальное число попыток.
- RFC 6585 — Additional HTTP Status Codes — IETF, апрель 2012 года, RFC 6585, Standards Track. Применение: Фиксирует статус 429 Too Many Requests и место сигнала о перегрузке сервера. Граница: Не говорит, какой delay выбрать и можно ли повторять конкретную операцию.