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:
3
stocks/README.md
Normal file
3
stocks/README.md
Normal 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).
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user