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

Долгий опрос в Spring MVC

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

1. Обзор

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

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

2. Долгий опрос с использованием DeferredResult

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

Наша предыдущая статья о классе Spring DeferredResult более подробно описывает его возможности и варианты использования.

2.1. Издатель

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

Сначала давайте определим Spring @RestController , который использует DeferredResult , но не переносит свою работу на другой рабочий поток:

@RestController
@RequestMapping("/api")
public class BakeryController {
@GetMapping("/bake/{bakedGood}")
public DeferredResult<String> publisher(@PathVariable String bakedGood, @RequestParam Integer bakeTime) {
DeferredResult<String> output = new DeferredResult<>();
try {
Thread.sleep(bakeTime);
output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));
} catch (Exception e) {
// ...
}
return output;
}
}

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

Давайте теперь установим вывод асинхронно, перенеся работу в рабочий поток:

private ExecutorService bakers = Executors.newFixedThreadPool(5);

@GetMapping("/bake/{bakedGood}")
public DeferredResult<String> publisher(@PathVariable String bakedGood, @RequestParam Integer bakeTime) {
DeferredResult<String> output = new DeferredResult<>();
bakers.execute(() -> {
try {
Thread.sleep(bakeTime);
output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));
} catch (Exception e) {
// ...
}
});
return output;
}

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

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

Для обработки проверенных ошибок, выдаваемых нашим воркером, мы будем использовать метод setErrorResult , предоставленный DeferredResult :

bakers.execute(() -> {
try {
Thread.sleep(bakeTime);
output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));
} catch (Exception e) {
output.setErrorResult("Something went wrong with your order!");
}
});

Рабочий поток теперь может изящно обрабатывать любое сгенерированное исключение.

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

DeferredResult<String> output = new DeferredResult<>(5000L);

Далее реализуем сценарий тайм-аута. Для этого мы будем использовать onTimeout:

output.onTimeout(() -> output.setErrorResult("the bakery is not responding in allowed time"));

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

2.2. Подписчик

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

Написать службу, которая вызывает этот API для длинных опросов, довольно просто, так как это по сути то же самое, что и написание клиента для стандартных блокирующих вызовов REST. Единственная реальная разница заключается в том, что мы хотим убедиться, что у нас есть механизм тайм-аута из-за времени ожидания длительного опроса. В Spring MVC мы можем использовать RestTemplate или WebClient для достижения этой цели, так как оба имеют встроенную обработку времени ожидания.

Во-первых, давайте начнем с примера, используя RestTemplate. Давайте создадим экземпляр RestTemplate с помощью RestTemplateBuilder , чтобы мы могли установить продолжительность тайм-аута:

public String callBakeWithRestTemplate(RestTemplateBuilder restTemplateBuilder) {
RestTemplate restTemplate = restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(10))
.setReadTimeout(Duration.ofSeconds(10))
.build();

try {
return restTemplate.getForObject("/api/bake/cookie?bakeTime=1000", String.class);
} catch (ResourceAccessException e) {
// handle timeout
}
}

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

Далее давайте создадим пример с помощью WebClient для достижения того же результата:

public String callBakeWithWebClient() {
WebClient webClient = WebClient.create();
try {
return webClient.get()
.uri("/api/bake/cookie?bakeTime=1000")
.retrieve()
.bodyToFlux(String.class)
.timeout(Duration.ofSeconds(10))
.blockFirst();
} catch (ReadTimeoutException e) {
// handle timeout
}
}

Наша предыдущая статья о настройке тайм-аутов Spring REST раскрывает эту тему более подробно.

3. Тестирование длинного опроса

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

MvcResult asyncListener = mockMvc
.perform(MockMvcRequestBuilders.get("/api/bake/cookie?bakeTime=1000"))
.andExpect(request().asyncStarted())
.andReturn();

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

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

String response = mockMvc
.perform(asyncDispatch(asyncListener))
.andReturn()
.getResponse()
.getContentAsString();

assertThat(response)
.isEqualTo("Bake for cookie complete and order dispatched. Enjoy!");

Используя asyncDispatch() , мы можем получить ответ асинхронного вызова и подтвердить его значение.

Чтобы протестировать механизм тайм-аута нашего DeferredResult , нам нужно немного изменить тестовый код, добавив активатор тайм-аута между asyncListener и ответными вызовами:

((MockAsyncContext) asyncListener
.getRequest()
.getAsyncContext())
.getListeners()
.get(0)
.onTimeout(null);

Этот код может показаться странным, но есть определенная причина, по которой мы вызываем onTimeout таким образом. Мы делаем это, чтобы сообщить AsyncListener , что время ожидания операции истекло. Это гарантирует, что класс Runnable , который мы реализовали для нашего метода onTimeout в нашем контроллере, вызывается правильно.

4. Вывод

В этой статье мы рассмотрели, как использовать DeferredResult в контексте длительного опроса. Мы также обсудили, как мы можем написать подписывающихся клиентов для длинных опросов и как это можно протестировать. Исходный код доступен на GitHub .