Как реализовать повторные попытки (retry) вызова внешнего сервиса при ошибках
Что проверяют
Понимаете ли вы, что retry это не «повторить три раза в цикле», а решение с последствиями: какие ошибки повторять, с какой паузой и не повредит ли повтор данным. Следом обычно спрашивают про идемпотентность и про то, как retry сочетается с circuit breaker.
Как отвечать
Каркас из пяти решений:
- что повторять: таймауты, обрывы соединения,
502,503,429. Ответы400,401,404повторять бессмысленно, сами они не исправятся; - сколько: ограниченное число попыток, обычно от трёх до пяти, плюс общий дедлайн на всю операцию;
- пауза: экспоненциальный backoff с джиттером (200 мс, 400, 800 плюс случайная добавка), чтобы сотня клиентов не ударила в сервис одновременно. Если сервер прислал
Retry-After, уважать его; - идемпотентность:
GETиPUTповторять безопасно,POSTна создание платежа нет. Для него нужен ключ идемпотентности в заголовке, по которому сервер распознает дубль; - чем делать: Resilience4j (
RetryConfig,IntervalFunction.ofExponentialRandomBackoff), Spring Retry с@Retryableи@Recover, в реактивном стекеRetry.backoff(3, Duration.ofMillis(200))из Reactor. В Spring Framework 7 аннотация@Retryableпоявилась прямо в ядре.
Самописный цикл допустим, но стоит сказать, что Thread.sleep занимает поток на всё ожидание. На виртуальных потоках Java 21 это дёшево, а на пуле платформенных потоков под нагрузкой быстро съедает пул. Поймав InterruptedException, нужно восстановить флаг и выйти, а не крутить цикл дальше.
Каждую повторную попытку стоит писать в лог и в метрику, иначе деградация сервиса прячется за ростом латентности.
Где спотыкаются
Повторяют всё подряд, включая 4xx и ошибки собственной сериализации, да ещё с фиксированной паузой. Сервис получает лишнюю нагрузку, а ответ не меняется.
Ставят retry на нескольких уровнях сразу: клиент, сервис, шлюз. Три слоя по три попытки дают 27 запросов на один клик, и упавший сервис добивают. Для этого и нужен circuit breaker, который перестаёт звонить после серии ошибок.
Вешают @Retryable на метод и вызывают его из того же класса. Вызов идёт мимо прокси, и повторов нет вовсе, а в тестах с моками этого не видно.