Ответ AsyncContext не соответствует исходному входящему запросу?

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

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

Входящий запрос для асинхронного обновления выглядит следующим образом:

@RequestMapping("/getDashboardStatus.json")
public void getDashboardStatus(HttpServletRequest request, ...) {
    final AsyncContext asyncContext = request.startAsync();
    // 10 seconds
    asyncContext.setTimeout(10000);
    asyncContext.start(new Runnable() {
        public void run() {
            // .. (code here waits for an update to occur) ..
            sendMostRecentDashboardJSONToResponse(asyncContext.getResponse());
            if (asyncContext.getRequest().isAsyncStarted()) {
                asyncContext.complete();
            }
        }
    });
}

Что странно, что на этой панели есть ссылки, которые идут на другие страницы. Каждые ~ 100 кликов или около того, один из них вместо отображения выбранной страницы фактически отображает отправленный JSON выше!

Например, у нас есть отдельный метод MVC:

@RequestMapping("/result/{resultId}")
public ModelAndView getResult(@PathVariable String resultId) {
    return new ModelAndView(...);
}

И при нажатии на ссылку на панели управления, которая посещает /result/1234, каждая синяя луна, страница загружается с статусом 200 OK, но вместо того, чтобы содержать ожидаемый HTML, на самом деле содержит JSON для запроса на опрос!

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

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

Я заметил метод hasOriginalRequestAndResponse() на объекте AsyncContext, но мне трудно понять Javadoc, независимо от того, что я ищу.

Обновление: Я просто добавил фрагмент так:

String requestURI = ((HttpServletRequest)asyncContext.getRequest()).getRequestURI());
System.out.println("Responding w/ Dashboard to: " + requestURI);
sendMostRecentDashboardJSONToResponse(asyncContext.getResponse(), clientProfileKey);

И смог воспроизвести проблему во время правильного поведения, я вижу:

Responding w/ Dashboard to: /app/getDashboardStatus.json

Но когда я вижу, что JSON нажата на запросы, инициированные кликом, я вижу:

Responding w/ Dashboard to: null

Ответ 1

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

Вызов startAsync() гарантирует, что объекты запроса/ответа не будут переработаны до тех пор, пока асинхронный контекст не будет завершен. Несмотря на то, что я не нашел места, где контекст будет завершен преждевременно или ошибочно, он заканчивается:

К таймауту.

Я смог последовательно воспроизвести эту проблему, ожидая 10+ секунд без активности на стороне сервера, позволяя запросу обновления JSON на время ожидания, а затем нажав на ссылку. После таймаута запрос/ответ, связанный с асинхронным контекстом, истекает и, таким образом, завершается и, таким образом, перерабатывается.

Было найдено два решения.

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

Второе - просто вызвать isAsyncStarted() по запросу до написания ответа. Если контекст исчерпан, этот метод вернет false. Если контекст все еще действителен/ждет, он вернет true.

Ответ 2

Если вы прочитали раздел Asynchronous Request Processing в справочных документах Spring, вы заметите, что можете упростить свое решение:

@RequestMapping(value = "/getDashboardStatus.json", 
                produces = MediaType.APPLICATION_JSON_VALUE)
public Callable<MostRecentDashboard> getDashboardStatus() {
    return new Callable() {
        @Override
        public MostRecentDashboard call() {
            // .. (code here waits for an update to occur) ..
            return ...;
        }
    });
}

Экземпляр MostRecentDashboard будет сериализован с использованием Jackson (при условии, что Jackson 2 находится в пути к классам).

Затем вам понадобится TaskExecutor, который выполнит Callable (он также обычно используется для настройки таймаута по умолчанию), читайте Spring MVC Async Config для параметров. В качестве альтернативы вы можете вернуть DeferredResult<MostRecentDashboard>, если Spring не знает поток, который присваивает значение MostRecentDashboard, например. экземпляры MostRecentDashboard считываются из JMS, Redis или аналогичных. Для получения дополнительной информации прочитайте сообщение Представляем сообщение Servlet 3, Async Support.

Начиная с Spring версии 4.1, вы можете вернуть ListenableFuture<MostRecentDashboard> непосредственно с вашего контроллера, что полезно, если вы извлечете MostRecentDashboard с помощью AsyncRestTemplate.