Skip directly to content

Building A High Throughput Web App with Spring-MVC and VoltDB

Thursday, June 21, 2012 - 12:00am

Written by Andrew Wilson

 

My last few posts have discussed parts of a web application that integrate VoltDB into a Spring web application. Today I will show how all the pieces are put together to build a low latency, high throughput Spring-MVC application. Much of my focus will be on the data layer where VoltDB resides, but I will go all the way up to the browser too.

 

The application is simple. It has two main parts. The first is a scheduled process that casts votes into VoltDB. Those votes simulate people calling in and voting for their favorite contestant in a talent show. The second part is the presentation layer that displays the contestants and the number of votes that each received. One web page, a little Java and Javascript with some XML and you have a quick application that shows how to build each layer of a web application from the client to the database.

 

Spring-MVC is one of my favorite frameworks. I like that I can configure each layer of my application through annotations or xml files. This application is using a single Java server page though I would have been just as happy to use Freemarker and could have done so with hardly any effort. I also like that Spring encourages me to write interfaces and concrete implementations, ensuring that my application is as loosely coupled as possible by dynamically wiring my components together. All this makes it easy for me to make minor or even sweeping changes with some measure of isolation, thus ensuring that my application will produce the same results regardless of the changes.

 

This application uses a Spring repository for managing the database. I use the Converter interface to map between the data and application layer objects that allows me to again, make significant changes to the database and queries without breaking the user interface. There is Jackson enabling JSON support for some browser side queries that update an html table. There are a pair of scheduled tasks that populate the database and report on database operation statistics. Those are the key components and some of them don’t require any code on the developer’s part.

 

Let’s begin at the top of the Spring-MVC layer, skipping the jsp.

 

@Controller

 

public class HomeController {

 

@Autowired

private VoterRepository voterRepository;

/**

* Simply selects the home view to render by returning its name.

*/

@RequestMapping(value = "/", method = RequestMethod.GET)

public String home(Locale locale, Model model) {

return "home";

}

@RequestMapping(value = "/results",

method = RequestMethod.GET,

produces="application/json")

public @ResponseBody

ElectionResults voterResults() throws Exception {

ElectionResults results = voterRepository.getResults();

return results;

}

}

 

The first method returns our home page and the second method returns a JSON response by calling the wired VoterRepository and returning the voting results. The ElectionResults object is converted behind the scenes using the Jackson library.

 

public interface VoterRepository extends Repository<VoltTable[], Long> {

 

// Call once during startup

 

void init(ProcedureCallback callback, int count, String candidates)

 

throws Exception;

 

// Call at the end of voting

 

void getResults(ProcedureCallback callback) throws Exception;

 

// Synchronous version of the above

 

ElectionResults getResults() throws Exception;

 

// Register each vote

 

void vote(ProcedureCallback callback, Vote vote) throws Exception;

 

//Get driver statistics

 

public ClientStatsContext createClientStats();

 

}

 

@Repository

 

public class VoterRepositoryImpl implements VoterRepository {

 

/**

 

* Automatically generated by the VoltClientFactoryBean The client connects

 

* to all the voltdb servers in the cluster and manages all stored procedure

 

* invocations.

 

*/

 

@Autowired

 

Client client;

 

/**

 

* The getResults() method returns an ElectionResults object, which nests

 

* individual CandidateResult objects. The conversion service is a

 

* convenient method for converter between the VoltDB specific VoltTable

 

* object to application specific value objects.

 

*/

 

@Autowired

 

private ConversionService conversionService;

 

// ...

 

/**

 

* Synchronous version of the above method and performs a conversion from a

 

* VoltTable to application specific value objects.

 

*

 

* @see org.voltdb.examples.repositories.VoterRepository#getResults()

 

*/

 

@Override

 

public ElectionResults getResults() throws Exception {

 

ElectionResults results = new ElectionResults();

 

ClientResponse response = this.client.callProcedure("Results");

 

VoltTable[] tables = response.getResults();

if (tables != null && tables.length > 0) {

 

VoltTable voteTable = tables[0];

 

results = this.conversionService.convert(voteTable,

 

ElectionResults.class);

 

}

 

return results;

 

}

 

// ...

 

}

 

The VoterRepository is just an interface and the VoterRepositoryImpl is the only concrete implementation in the application. I could support multiple databases by creating additionalVoterRepository objects and configuring them within the servlet-context.xml file.  Regardless that there is only one implementation, the HomeController just gets back an instance of aVoterRepository and that repository could be anything.

 

The getResults() method returns an instance of ElectionResults by using a Spring Converter. The Spring Converter maps the VoltDB VoltTable result into an instance of ElectionResults. Again, the ElectionResults object need not know anything about the VoltTable thanks to the converter.

The code is organized such that I can easily replace one data source for another. I could find out that one database does not meet my needs and can swap it out for another without having to change my entire application. I only need to change the repository and converter implementations to migrate my application.

 

Although Spring offers a lot of application portability, I should not understate the challenges involved in a migration from one database to another. You may have to rewrite more than just a repository or a service. You may have stored procedures, complicated queries using vendor specific extensions to SQL, schema constructs that are not supported by the new database and many other issues. The goal here is to minimize the changes to the services that directly access the database without having to modify the application and presentation layers, which can be very complex and very difficult to debug.

 

The application must also register votes. We do this using a scheduled task and discuss it in  Using the Spring @Schedule Annotation. There are two tasks, one for registering votes and one for gather statistics so we can measure the transaction throughput. These two tasks run asynchronously to the rest of the application.

 

We’ve covered all the major components except for configuration. Let’s look at the servlet-context.xml.

 

http://www.springframework.org/schema/mvc

 

http://www.springframework.org/schema/mvc/spring-mvc-3.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/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> 

 

There are three important configurations. The first is highlighted in red and turns on annotation scanning for the MVC layer and the tasks. These are configured separately and can cause quite a bit of confusion because it is reasonable to think that the tasks would be configured through the MVC scanner, but they are not.

 

The second block in blue configures the VoltDB client to connect to a VoltDB server running on the localhost. The hostsnames property is a comma delimited list that can be expressed as server1,server2 instead of pointing only to localhost.

 

The last configuration in green adds a list of converter objects to theConversionServiceFactory. The factory instantiates the converter based upon the types that I want from and to. The line “results = this.conversionService.convert(voteTable, ElectionResults.class);” specifies the source object of VoltTable and the targetElectionResults class. The factory will find a matching converter and execute the conversion. Let’s look at the converter. 

public class VoltTableToElectionResultsConverter implements

 

Converter<VoltTable, ElectionResults> {

 

@Autowired

 

ApplicationContext context;

 

@Override

public ElectionResults convert(VoltTable table) {

 

// The following gets around an issue where this converter

 

// depends on another converter

 

ConversionService conversionService = (ConversionService) context

 

.getBean("conversionService");

 

ElectionResults voteResults = new ElectionResults();

 

CandidateResult[] candidateResults = new CandidateResult[table

 

.getRowCount()];

 

voteResults.setCandidateResults(candidateResults);

 

int index = 0;

 

while (table.advanceRow()) {

 

candidateResults[index++] = conversionService.convert(table,

 

CandidateResult.class);

 

}

 

return voteResults;

 

}

 

}

The VoltTableToElectionResultsConverter class is somewhat different from a typical converter. This converter can handle hierarchies of objects. The ElectionResults object contains a collection of CandidateResult objects. We want to use a converter to map theVoltTable result to a CandidateResult. Note that we wire the ApplicationContext object rather than the conversion service. We then invoke the ConversionService by getting its bean. Normally you would just wire the ConversionService directly and then you’ll get a bunch of exceptions during startup because you are creating a circular dependency. TheConversionService cannot complete initialization without fully initializing theVoltTableToElectionResultsConverter, which would typically require a fully initializedConversionService. Consequently, we work around the problem by going through theApplicationContext, thus eliminating the autowire of the ConversionService.

 

The Converter API is described in greater detail in Using the Spring Converter API with VoltDB Data Objects.

This looks like a lot of code for doing very little work and that is true. There is a lot of boilerplate code, the fixed cost of writing any application. The cost of the effort goes down as your application gets bigger.

 

The application runs under Tomcat and is rather fast. In fact, it runs only slightly slower than a command line application designed to “firehose” the database. In the very near future I will post what happened when we benchmarked this applicatiom connecting to VoltDB running on an Amazon EC2 cluster. The results were very surprising and easy to replicate.