Add Spring MVC 3.2 Servlet based async support

Instead of polling at regular intervals for quotes, the server now
holds up the response until a new quote is available, which results
in fewer requests overall and more optimal latency.

Similarly instead of returning a trade confirmation id and then
polling to obtain the confirmation, the server now holds up the
request and until the confirmation becomes available. Since the
request processing is released immediately, the longer wait should
not affect the ability of the server to process requests while
also eliminating the need for polling
This commit is contained in:
Rossen Stoyanchev
2012-04-18 02:15:40 -04:00
parent 772d1fbe21
commit 0530c6b61b
8 changed files with 119 additions and 87 deletions

3
stocks/README.md Normal file
View File

@@ -0,0 +1,3 @@
This branch contains a version of the Spring AMQP stocks sample modified to take advantage of Spring MVC 3.2, Servlet-based async support. The change shows how an existing application with client-side polling can improve its latency while also optimizing the number of requests required to deliver updates.
The details of the changes can be viewed in the following [commit](https://github.com/SpringSource/spring-amqp-samples/commit/1a241f8cd68835fa9e6af4e987bccf7b9e6b8bf1).

View File

@@ -15,7 +15,7 @@
</description>
<properties>
<maven.test.failure.ignore>true</maven.test.failure.ignore>
<spring.framework.version>3.0.5.RELEASE</spring.framework.version>
<spring.framework.version>3.2.0.BUILD-SNAPSHOT</spring.framework.version>
<spring.amqp.version>1.0.0.RELEASE</spring.amqp.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -34,11 +34,23 @@
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-amqp</artifactId>
<version>${spring.amqp.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>${spring.amqp.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@@ -69,9 +81,9 @@
<version>${spring.framework.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-servlet_3.0_spec</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -132,6 +144,13 @@
</dependencies>
<repositories>
<repository>
<id>SpringSource repository</id>
<url>http://repo.springsource.org/milestone</url>
</repository>
</repositories>
<build>
<pluginManagement>
<plugins>

View File

@@ -27,16 +27,16 @@ import org.springframework.amqp.rabbit.stocks.service.ExecutionVenueService;
/**
* Execute the trade, setting the execution price to changing value in line with what the market data feed is producing.
*
*
* @author Mark Pollack
*
*/
public class ExecutionVenueServiceStub implements ExecutionVenueService {
private static Log log = LogFactory.getLog(ExecutionVenueServiceStub.class);
private Random random = new Random();
public TradeResponse executeTradeRequest(TradeRequest request) {
TradeResponse response = new TradeResponse();
response.setAccountName(request.getAccountName());
@@ -46,11 +46,11 @@ public class ExecutionVenueServiceStub implements ExecutionVenueService {
response.setTicker(request.getTicker());
response.setRequestId(request.getId());
response.setConfirmationNumber(UUID.randomUUID().toString());
try {
log.info("Sleeping 2 seconds to simulate processing..");
Thread.sleep(2000);
log.info("Sleeping 5 seconds to simulate processing..");
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error("Didn't finish sleeping", e);
}
@@ -68,11 +68,11 @@ public class ExecutionVenueServiceStub implements ExecutionVenueService {
{
//in line with market data implementation
return new BigDecimal(22 + Math.abs(gaussian()));
}
}
}
private double gaussian() {
return random.nextGaussian();
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2002-2010 the original author or authors.
*
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
@@ -18,6 +18,8 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -36,10 +38,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;
/**
* @author Dave Syer
*
* @author Rossen Stoyanchev
*/
@Controller
public class QuoteController {
@@ -52,6 +55,10 @@ public class QuoteController {
private Queue<Quote> quotes = new PriorityBlockingQueue<Quote>(100, new QuoteComparator());
private Map<String, DeferredResult> suspendedTradeRequests = new ConcurrentHashMap<String, DeferredResult>();
private Map<DeferredResult, Long> suspendedQuoteRequests = new ConcurrentHashMap<DeferredResult, Long>();
private long timeout = 30000; // 30 seconds of data
public void setStockServiceGateway(StockServiceGateway stockServiceGateway) {
@@ -63,17 +70,34 @@ public class QuoteController {
String key = response.getRequestId();
responses.putIfAbsent(key, response);
Collection<TradeResponse> queue = new ArrayList<TradeResponse>(responses.values());
long timestamp = System.currentTimeMillis() - timeout;
for (Iterator<TradeResponse> iterator = queue.iterator(); iterator.hasNext();) {
TradeResponse tradeResponse = iterator.next();
String requestId = tradeResponse.getRequestId();
if (tradeResponse.getTimestamp() < timestamp) {
responses.remove(tradeResponse.getRequestId());
responses.remove(requestId);
}
if (suspendedTradeRequests.containsKey(requestId)) {
DeferredResult deferredResult = suspendedTradeRequests.remove(requestId);
deferredResult.trySet(tradeResponse);
}
}
}
public void handleQuote(Quote message) {
logger.info("Client received: " + message);
quotes.add(message);
for (Entry<DeferredResult, Long> entry : suspendedQuoteRequests.entrySet()) {
List<Quote> list = getLatestQuotes(entry.getValue());
if (!list.isEmpty()) {
DeferredResult deferredResult = entry.getKey();
deferredResult.trySet(list);
suspendedQuoteRequests.remove(entry.getKey());
}
}
long timestamp = System.currentTimeMillis() - timeout;
for (Iterator<Quote> iterator = quotes.iterator(); iterator.hasNext();) {
Quote quote = iterator.next();
@@ -81,12 +105,23 @@ public class QuoteController {
iterator.remove();
}
}
quotes.add(message);
}
@RequestMapping("/quotes")
@ResponseBody
public List<Quote> quotes(@RequestParam(required = false) Long timestamp) {
public Object quotes(@RequestParam(required = false) Long timestamp) {
List<Quote> list = getLatestQuotes(timestamp);
if (list.isEmpty()) {
DeferredResult deferredResult = new DeferredResult(Collections.emptyList());
suspendedQuoteRequests.put(deferredResult, timestamp);
return deferredResult;
}
else {
return list;
}
}
private List<Quote> getLatestQuotes(Long timestamp) {
if (timestamp == null) {
timestamp = 0L;
}
@@ -102,13 +137,16 @@ public class QuoteController {
@RequestMapping(value = "/trade", method = RequestMethod.POST)
@ResponseBody
public TradeRequest trade(@ModelAttribute TradeRequest tradeRequest) {
public Object trade(@ModelAttribute TradeRequest tradeRequest) {
String ticker = tradeRequest.getTicker();
Long quantity = tradeRequest.getQuantity();
if (quantity == null || quantity <= 0 || !StringUtils.hasText(ticker)) {
// error
return tradeRequest;
return null;
} else {
DeferredResult deferredResult = new DeferredResult();
suspendedTradeRequests.put(tradeRequest.getId(), deferredResult);
// Fake rest of request while UI is basic
tradeRequest.setAccountName("ACCT-123");
tradeRequest.setBuyRequest(true);
@@ -117,15 +155,9 @@ public class QuoteController {
tradeRequest.setUserName("Joe Trader");
tradeRequest.setUserName("Joe");
stockServiceGateway.send(tradeRequest);
}
return tradeRequest;
}
@RequestMapping(value = "/trade", method = RequestMethod.GET)
@ResponseBody
public TradeResponse response(@RequestParam String requestId) {
TradeResponse result = responses.get(requestId);
return result;
return deferredResult;
}
}
private static class QuoteComparator implements Comparator<Quote> {

View File

@@ -6,6 +6,6 @@ log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
#log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
log4j.appender.stdout.layout.ConversionPattern=%-5p %8t [%40.40c{4}]: %m%n
log4j.category.org.springframework.amqp.rabbit=DEBUG
log4j.category.org.springframework.amqp.rabbit=INFO
log4j.category.org.springframework.beans.factory=INFO

View File

@@ -5,7 +5,7 @@
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<bean id="stockServiceGateway" class="org.springframework.amqp.rabbit.stocks.gateway.RabbitStockServiceGateway">
<property name="rabbitTemplate">
@@ -53,4 +53,12 @@
<mvc:annotation-driven />
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="order" value="0"/>
<property name="asyncRequestTimeout" value="30000"/>
<property name="messageConverters">
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
</property>
</bean>
</beans>

View File

@@ -1,10 +1,8 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/server-bootstrap-*.xml</param-value>
@@ -17,11 +15,13 @@
<filter>
<filter-name>shallowEtagHeaderFilter</filter-name>
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
@@ -42,6 +42,7 @@
<param-value>classpath*:/servlet-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>

View File

@@ -14,25 +14,18 @@
var timer;
var debug = false;
var lastquote = 0;
var lasttrades = {};
var template = "{{#quotes}}<tr>\
<td>{{timeString}}</td>\
<td>{{#stock}}{{ticker}}{{/stock}}</td>\
<td>{{price}}</td>\
</tr>{{/quotes}}";
var confirmation = "{{#response}}Trade Confirmation: <ul>\
<li>Id: {{confirmationNumber}}</li>\
<li>Quantity: {{quantity}}</li>\
<li>Ticker: {{ticker}}</li>\
<li>Price: {{price}}</li>\
</ul>{{/response}}";
var confirmation = "{{#response}}Trade confirmation {{confirmationNumber}}, Quantity: {{quantity}}, Ticker: {{ticker}}, Price: {{price}}{{/response}}<br>";
function load() {
if (running) {
$('#status').text("Waiting...")
$('#status').text("Getting quotes...")
$.ajax({
url : "quotes?timestamp=" + lastquote,
success : function(message) {
$('#status').text("Updating")
if (debug) {
$('#debug').text(JSON.stringify(message))
}
@@ -79,46 +72,23 @@
}
return setTimeout(load, 1000);
}
function confirm(id) {
if (lasttrades.id) {
clearTimeout(lasttrades.id);
delete lasttrades.id;
}
$.get("trade?requestId=" + id, function(response) {
if (response && response.requestId) {
$('#messages').html($.mustache(confirmation, {
response : response
}));
delete lasttrades.id;
} else {
lasttrades.id = setTimeout("confirm('" + id + "')", 2000);
}
});
}
$(function() {
$.ajaxSetup({cache:false});
$('#start').click(start);
$('#stop').click(stop);
$('#clear').click(clear);
start();
$('#tradeForm')
.submit(
function() {
$
.post(
$('#tradeForm').attr("action"),
$('#tradeForm').serialize(),
function(request) {
var message = "Processing...";
if (request && request.ticker) {
confirm(request.id);
} else {
message = "The trade request was invalid. Please provide a quantity and a stock ticker.";
}
$('#messages').text(message);
});
return false;
});
$('#tradeForm').submit(function() {
$('#messages').text('');
$.post($('#tradeForm').attr("action"), $('#tradeForm').serialize(),
function(response) {
if (response && response.requestId) {
$('#confirmations').append($.mustache(confirmation, {response : response}));
} else {
$('#messages').html("<p>The trade request was invalid. Please provide a quantity and a stock ticker.</p>");
}
});
return false;
});
});
</script>
</head>
@@ -156,6 +126,7 @@
<c:set var="ticker" value="" />
</c:otherwise>
</c:choose>
<div id="messages"></div>
<form id="tradeForm" method="post" action="trade">
<ol>
<li><label for="ticker">Quantity</label><input id="quantity"
@@ -167,9 +138,7 @@
</li>
</ol>
</form>
<div id="messages">
<form:errors path="*" cssClass="errors" />
</div>
<div id="confirmations"></div>
<h1>Quotes</h1>
<div id="status">Stopped</div>
<br />