Рубрики
Без рубрики

Установка тайм-аута запроса для API Spring REST

Автор оригинала: Jordan Simpson.

1. Обзор

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

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

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

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

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

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

public interface BookRepository extends JpaRepository {

    default int wasteTime() {
        int i = Integer.MIN_VALUE;
        while(i < Integer.MAX_VALUE) {
            i++;
        }
        return i;
    }
}

Если мы вызовем наш метод waste Time () , находясь внутри транзакции с тайм-аутом в 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.");
}

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

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

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

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

3. Ограничитель Времени Устойчивости

Resilience 4j-это библиотека , в первую очередь предназначенная для управления отказоустойчивостью удаленных коммуникаций. Это Ограничитель времени модуль – это то, что нас здесь интересует.

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


    io.github.resilience4j
    resilience4j-timelimiter
    1.6.1

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

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

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

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

@GetMapping("/author/resilience4j")
public Callable 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.");
    }));
}

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

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

4. Весенний запрос MVC-тайм-аут

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

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

spring.mvc.async.request-timeout=750

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

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

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

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

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

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

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

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


    org.springframework.boot
    spring-boot-starter-webflux
    2.4.2

Давайте определим Веб-клиент с таймаутом ответа 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-адрес извне, а также несколько других дополнительных свойств.

Теперь мы можем внедрить наш веб-клиент в ваш контроллер и использовать его для вызова нашей собственной /транзакционной конечной точки, у которой все еще есть тайм-аут в 1 секунду. Поскольку мы настроили наш веб-клиент на тайм-аут в 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();
}

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

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

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

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

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

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