Engineering
Releases
News and Events

Spring MVC 3.2 Preview: Adding Long Polling to an Existing Web Application

Last updated on November 5th, 2012 (Spring MVC 3.2 RC1)

In my last post I discussed how to make a Spring MVC controller method asynchronous by returning a Callable which is then invoked in a separate thread by Spring MVC.

But what if async processing depended on receiving some external event in a thread not known to Spring MVC -- e.g. receiving a JMS message, an AMQP message, a Redis pub-sub notification, a Spring Integration event, and so on? I'll explore this scenario by modifying an existing sample from the Spring AMQP project.

The Sample

Spring AMQP has a stock trading sample where a QuoteController sends trade execution messages via Spring AMQP's RabbitTemplate and receives trade confirmation and price quote messages via Spring AMQP's RabbitMQ listener container in message-driven POJO style.

In the browser, the sample uses polling to display price quotes. For trades, the initial request submits the trade and a confirmation id is returned that is then used to poll for the final confirmation. I've updated the sample to take advantage of the Spring 3.2 Servlet 3 async support. The master branch has the code before and the spring-mvc-async branch has the code after the change. The images below show the effect on the frequency of price quote requests (using the Chrome developer tools):

Before the change: traditional polling

After the change: long poll

As you can see with regular polling new requests are sent very frequently (milliseconds apart) while with long polling, requests can be 5, 10, 20, or more seconds apart -- a significant reduction in the total number of requests without the loss of latency, i.e. the amount of time before a new price quote appears in the browser.

Getting Quotes

So what changes were required? From a client perspective traditional polling and long polling are indistinguishable so the HTML and JavaScript did not change. From a server perspective requests must be held up until new quotes arrive. This is how the controller processes a request for quotes:



// Class field private Map<String, DeferredResult> suspendedTradeRequests = new ConcurrentHashMap<String, DeferredResult>(); ... @RequestMapping("/quotes") @ResponseBody public DeferredResult<List<Quote>> quotes(@RequestParam(required = false) Long timestamp) { final DeferredResult<List<Quote>> result = new DeferredResult<List<Quote>>(null, Collections.emptyList()); this.quoteRequests.put(result, timestamp); result.onCompletion(new Runnable() { public void run() { quoteRequests.remove(result); } }); List<Quote> list = getLatestQuotes(timestamp); if (!list.isEmpty()) { result.setResult(list); } return result; }

In the example above the controller method prepares and returns a DeferredResult, which it can set immediately, if quotes are already available, or later when new quotes are received via RabbitMQ. The DeferredResult is saved in a Map from where it will be removed when the async request completes by the registered onCompletion callback.

And here is the controller method that updates saved DeferredResult instances when new quotes are received:



// Invoked in Spring AMQP's RabbitMQ listener container thread public void handleQuote(Quote message) { // ... for (Entry<DeferredResult<List<Quote>>, Long> entry : this.quoteRequests.entrySet()) { List<Quote> newQuotes = getLatestQuotes(entry.getValue()); entry.getKey().setResult(newQuotes); } // ... }

When new quotes arrive, the above method updates each saved DeferredResult with the latest quotes. Since the DeferredResult was originally created in an @ResponseBody method, the quotes are written to the body of the response as JSON.

Timeouts

What if the async request associated with a DeferredResult times out? From a browser perspective each request should bring quotes and if a timeout is reached, 0 quotes should be returned.

You may have noticed in the sample code above that the DeferredResult was created with two constructor arguments. The first is the timeout value to use and the second is the default result to use if a timeout occurs, in this case an empty list.

Executing Trades

The changes required for trade executions follow a similar pattern. Instead of sending one request to execute the trade and then polling for the confirmation, a single request submits the trade and then holds for the confirmation.

The next and last post introduces a persistent chat sample.

comments powered by Disqus