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

Установка времени ожидания запроса для Spring REST API

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

1. Обзор

В этом руководстве мы рассмотрим несколько возможных способов реализации тайм-аутов запросов для Spring REST API.

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

2. @Транзакционные тайм- ауты

Один из способов, которым мы можем реализовать тайм-аут запроса для вызовов базы данных, — это воспользоваться аннотацией Spring @Transactional . У него есть свойство тайм -аута , которое мы можем установить. Значение по умолчанию для этого свойства равно -1, что эквивалентно полному отсутствию времени ожидания. Для внешней настройки значения тайм-аута вместо этого необходимо использовать другое свойство — timeoutString .

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

Чтобы увидеть это в действии, давайте напишем очень простой уровень репозитория JPA, который будет представлять внешнюю службу, выполнение которой занимает слишком много времени и вызывает тайм-аут. В этом расширении JpaRepository есть затратный по времени метод:

public interface BookRepository extends JpaRepository<Book, String> {

default int wasteTime() {
Stopwatch watch = Stopwatch.createStarted();

// delay for 2 seconds
while (watch.elapsed(SECONDS) < 2) {
int i = Integer.MIN_VALUE;
while (i < Integer.MAX_VALUE) {
i++;
}
}
}
}

Если мы вызовем наш метод WasteTime() внутри транзакции с тайм-аутом в 1 секунду, тайм-аут истечет до того, как метод завершит выполнение:

@GetMapping("/author/transactional")
@Transactional(timeout = 1)
public String getWithTransactionTimeout(@RequestParam String title) {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
}

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

Однако у этого решения с тайм-аутом есть несколько недостатков.

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

Рассмотрим некоторые другие варианты.

3. Ограничитель времени Resilience4j

Resilience4j — это библиотека, в первую очередь предназначенная для управления отказоустойчивостью удаленных коммуникаций. Его модуль TimeLimiter — это то, что нас здесь интересует.

Во- первых, мы должны включить в наш проект зависимость resilience4j-timelimiter :

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
<version>1.6.1</version>
</dependency>

Далее давайте определим простой TimeLimiter с длительностью тайм-аута 500 миллисекунд:

private TimeLimiter ourTimeLimiter = TimeLimiter.of(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(500)).build());

Это может быть легко настроено извне.

Мы можем использовать наш TimeLimiter , чтобы обернуть ту же логику, что и наш пример @Transactional :

@GetMapping("/author/resilience4j")
public Callable<String> getWithResilience4jTimeLimiter(@RequestParam String title) {
return TimeLimiter.decorateFutureSupplier(ourTimeLimiter, () ->
CompletableFuture.supplyAsync(() -> {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
}));
}

TimeLimiter предлагает несколько преимуществ по сравнению с решением @Transactional . А именно, он поддерживает точность до доли секунды и немедленное уведомление о тайм-ауте. Тем не менее, его по-прежнему необходимо вручную включать во все конечные точки, для которых требуется тайм-аут, он требует некоторого длинного кода-обертки, а ошибка, которую он создает, по-прежнему является общей ошибкой 500 HTTP. Кроме того, требуется вернуть Callable<String> вместо необработанной строки. ``

TimeLimiter включает в себя только подмножество функций Resilience4j и хорошо взаимодействует с шаблоном прерывателя цепи .

4. Время ожидания запроса Spring MVC ``

Spring предоставляет нам свойство с именем spring.mvc.async.request-timeout . Это свойство позволяет нам определить время ожидания запроса с точностью до миллисекунды.

Давайте определим свойство с тайм-аутом 750 миллисекунд:

spring.mvc.async.request-timeout=750

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

@GetMapping("/author/mvc-request-timeout")
public Callable<String> getWithMvcRequestTimeout(@RequestParam String title) {
return () -> {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
};
}

Мы видим, что код стал менее подробным, а конфигурация автоматически реализуется Spring, когда мы определяем свойство приложения. Ответ возвращается сразу после достижения тайм-аута, и он даже возвращает более описательную ошибку HTTP 503 вместо общей 500. Кроме того, каждая конечная точка в нашем проекте автоматически наследует эту конфигурацию тайм-аута.

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

5. Тайм- ауты веб- клиента

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

Также можно настроить тайм-ауты для более старого объекта Spring RestTemplate . Однако сейчас большинство разработчиков предпочитают WebClient RestTemplate . ``

Чтобы использовать WebClient, мы должны сначала добавить в наш проект зависимость Spring WebFlux :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.4.2</version>
</dependency>

Давайте определим WebClient с тайм-аутом ответа 250 миллисекунд, который мы можем использовать для вызова самих себя через localhost в его базовом URL-адресе:

@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().responseTimeout(Duration.ofMillis(250))
))
.build();
}

Ясно, что мы могли бы легко настроить это значение тайм-аута извне. Мы также можем настроить базовый URL-адрес извне, а также несколько других необязательных свойств.

Теперь мы можем внедрить наш WebClient в наш контроллер и использовать его для вызова нашей собственной конечной точки /transactional , которая по-прежнему имеет тайм-аут в 1 секунду. Поскольку мы настроили наш WebClient на тайм-аут в 250 миллисекунд, мы должны увидеть, что он выходит из строя намного быстрее, чем через 1 секунду.

Вот наша новая конечная точка:

@GetMapping("/author/webclient")
public String getWithWebClient(@RequestParam String title) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/author/transactional")
.queryParam("title", title)
.build())
.retrieve()
.bodyToMono(String.class)
.block();
}

После вызова этой конечной точки мы видим, что мы получаем тайм-аут WebClient в виде ответа об ошибке 500 HTTP. Мы также можем проверить журналы, чтобы увидеть тайм-аут @Transactional нисходящего потока . Но, конечно, его тайм-аут был бы напечатан удаленно, если бы мы вызывали внешний сервис вместо локального хоста.

Может потребоваться настройка разных тайм-аутов запросов для разных серверных служб, и это возможно с помощью этого решения. Кроме того, издатели ответов Mono или Flux , возвращаемые WebClient , содержат множество методов обработки ошибок для обработки общего ответа об ошибке тайм-аута.

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

В этой статье мы только что рассмотрели несколько различных решений для реализации тайм-аута запроса. Есть несколько факторов, которые следует учитывать при принятии решения о том, какой из них использовать.

Если мы хотим установить тайм-аут для наших запросов к базе данных, мы можем использовать метод Spring @Transactional и его свойство timeout . Если мы пытаемся интегрироваться с более широким шаблоном прерывателя цепи, использование TimeLimiter Resilience4j имело бы смысл. Использование свойства Spring MVC request-timeout лучше всего подходит для установки глобального тайм-аута для всех запросов, но мы можем легко определить более детальные тайм-ауты для каждого ресурса с помощью WebClient .

Для рабочего примера всех этих решений код готов и готов к запуску из коробки на GitHub .