Перейти к основному содержимому

Руководство по повторной попытке в Spring WebFlux

· 5 мин. чтения

1. Обзор

Когда мы создаем приложения в распределенной облачной среде, нам нужно проектировать их с учетом сбоев. Это часто включает повторные попытки.

Spring WebFlux предлагает нам несколько инструментов для повторения неудачных операций.

В этом руководстве мы рассмотрим, как добавлять и настраивать повторы в наших приложениях Spring WebFlux.

2. Вариант использования

В нашем примере мы будем использовать MockWebServer и моделировать внешнюю систему, которая временно недоступна, а затем становится доступной.

Давайте создадим простой тест для компонента, подключающегося к этой службе REST:

@Test
void givenExternalServiceReturnsError_whenGettingData_thenRetryAndReturnResponse() {

mockExternalService.enqueue(new MockResponse()
.setResponseCode(SERVICE_UNAVAILABLE.code()));
mockExternalService.enqueue(new MockResponse()
.setResponseCode(SERVICE_UNAVAILABLE.code()));
mockExternalService.enqueue(new MockResponse()
.setResponseCode(SERVICE_UNAVAILABLE.code()));
mockExternalService.enqueue(new MockResponse()
.setBody("stock data"));

StepVerifier.create(externalConnector.getData("ABC"))
.expectNextMatches(response -> response.equals("stock data"))
.verifyComplete();

verifyNumberOfGetRequests(4);
}

3. Добавление повторов

В API Mono и Flux встроены два ключевых оператора повтора .

3.1. Использование повтора

Во-первых, давайте воспользуемся методом retry , который не позволяет приложению немедленно возвращать ошибку и переподписывается заданное количество раз:

public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retry(3);
}

Это будет повторяться до трех раз, независимо от того, какая ошибка возвращается от веб-клиента.

3.2. Использование retryWhen

Далее попробуем настраиваемую стратегию с использованием метода retryWhen :

public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.max(3));
}

Это позволяет нам настроить объект Retry для описания желаемой логики.

Здесь мы использовали максимальную стратегию, чтобы повторить максимальное количество попыток. Это эквивалентно нашему первому примеру, но дает нам больше возможностей для настройки. В частности, следует отметить, что в этом случае каждая повторная попытка происходит максимально быстро .

4. Добавление задержки

Основным недостатком повторной попытки без какой-либо задержки является то, что это не дает сбойному сервису времени для восстановления. Это может перегрузить его, усугубив проблему и снизив шансы на выздоровление.

4.1. Повторная попытка с фиксированной задержкой

Мы можем использовать стратегию fixedDelay , чтобы добавить задержку между каждой попыткой:

public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(2)));
}

Эта конфигурация допускает двухсекундную задержку между попытками, что может увеличить шансы на успех. Однако, если сервер испытывает более длительный сбой, нам следует подождать дольше. Но если мы настроим все задержки на длительное время, короткие сообщения еще больше замедлят работу нашего сервиса.

4.2. Повторная попытка с отсрочкой

Вместо повторных попыток через фиксированные промежутки времени мы можем использовать стратегию отсрочки :

public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2)));
}

По сути, это добавляет постепенно увеличивающуюся задержку между попытками — в нашем примере примерно через 2, 4, а затем 8 секунд. Это дает внешней системе больше шансов восстановиться после обычных проблем с подключением или справиться с невыполненной работой.

4.3. Повторная попытка с дрожанием

Дополнительным преимуществом стратегии отсрочки является то, что она добавляет случайность или дрожание к вычисляемому интервалу задержки. Следовательно, джиттер может помочь уменьшить количество повторных попыток, когда несколько клиентов повторяют попытку одновременно .

По умолчанию это значение установлено на 0,5, что соответствует дрожанию не более 50% вычисленной задержки.

Давайте используем метод джиттера , чтобы настроить другое значение 0,75 для представления джиттера не более 75% вычисленной задержки:

public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2)).jitter(0.75));
}

Следует отметить, что возможный диапазон значений находится между 0 (отсутствие джиттера) и 1 (джиттер не более 100% вычисленной задержки).

5. Фильтрация ошибок

На этом этапе любые ошибки службы приведут к повторной попытке, включая ошибки 4xx, такие как 400:Bad Request или 401:Unauthorized .

Очевидно, что мы не должны повторять такие ошибки клиента, поскольку ответ сервера не будет отличаться. Поэтому давайте посмотрим, как мы можем применить стратегию повтора только в случае конкретных ошибок .

Во-первых, давайте создадим исключение для представления ошибки сервера:

public class ServiceException extends RuntimeException {

public ServiceException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
}

Далее мы создадим ошибку Mono с нашим исключением для ошибок 5xx и используем метод фильтра для настройки нашей стратегии:

public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.onStatus(HttpStatus::is5xxServerError,
response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(5))
.filter(throwable -> throwable instanceof ServiceException));
}

Теперь мы повторяем попытку только тогда, когда в конвейере WebClient возникает исключение ServiceException . ``

6. Обработка исчерпанных повторных попыток

Наконец, мы можем учесть вероятность того, что все наши повторные попытки были безуспешными. В этом случае поведение по умолчанию стратегии заключается в распространении RetryExhaustedException , оборачивая последнюю ошибку.

Вместо этого давайте переопределим это поведение с помощью метода onRetryExhaustedThrow и предоставим генератор для нашего ServiceException :

public Mono<String> getData(String stockId) {
return webClient.get()
.uri(PATH_BY_ID, stockId)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(5))
.filter(throwable -> throwable instanceof ServiceException)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
throw new ServiceException("External Service failed to process after max retries", HttpStatus.SERVICE_UNAVAILABLE.value());
}));
}

Теперь запрос завершится ошибкой с нашим ServiceException в конце неудачной серии повторных попыток.

7. Заключение

В этой статье мы рассмотрели, как добавить повторные попытки в приложение Spring WebFlux с помощью методов retry и retryWhen .

Изначально мы добавили максимальное количество повторных попыток для неудачных операций. Затем мы ввели задержку между попытками, используя и настроив различные стратегии.

Наконец, мы рассмотрели повторные попытки при определенных ошибках и настройку поведения, когда все попытки были исчерпаны.

Как всегда, полный исходный код доступен на GitHub .