Browse Source

Merge branch 'master' of gitlab.techempower.com:techempower/frameworkbenchmarks

Keith 12 years ago
parent
commit
1eb9088097

+ 29 - 1
undertow/benchmark_config

@@ -3,10 +3,38 @@
   "tests": [{
   "tests": [{
     "default": {
     "default": {
       "setup_file": "setup",
       "setup_file": "setup",
-      "json_url": "/",
+      "json_url": "/json",
       "plaintext_url": "/plaintext",
       "plaintext_url": "/plaintext",
+      "cache_url": "/cache",
       "port": 8080,
       "port": 8080,
       "sort": 120
       "sort": 120
+    },
+    "mysql" : {
+      "setup_file": "setup",
+      "db_url": "/db",
+      "query_url": "/db/mysql?queries=",
+      "fortune_url": "/fortunes/mysql",
+      "update_url": "/updates/mysql?queries=",
+      "port": 8080,
+      "sort": 121
+    },
+    "postgresql" : {
+      "setup_file": "setup",
+      "db_url": "/db",
+      "query_url": "/db/postgresql?queries=",
+      "fortune_url": "/fortunes/postgresql",
+      "update_url": "/updates/postgresql?queries=",
+      "port": 8080,
+      "sort": 122
+    },
+    "mongodb" : {
+      "setup_file": "setup",
+      "db_url": "/db",
+      "query_url": "/db/mongodb?queries=",
+      "fortune_url": "/fortunes/mongodb",
+      "update_url": "/updates/mongodb?queries=",
+      "port": 8080,
+      "sort": 123
     }
     }
   }]
   }]
 }
 }

+ 115 - 89
undertow/pom.xml

@@ -1,93 +1,119 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
 
 
-  <modelVersion>4.0.0</modelVersion>
+    <groupId>com.techempower</groupId>
+    <artifactId>undertow-example</artifactId>
+    <version>0.1</version>
 
 
-  <groupId>com.techempower</groupId>
-  <artifactId>undertow-example</artifactId>
-  <version>0.1</version>
+    <dependencies>
+        <!-- Web server -->
+        <dependency>
+            <groupId>io.undertow</groupId>
+            <artifactId>undertow-core</artifactId>
+            <version>1.0.0.Beta1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.xnio</groupId>
+            <artifactId>xnio-api</artifactId>
+            <version>3.1.0.CR5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.xnio</groupId>
+            <artifactId>xnio-nio</artifactId>
+            <version>3.1.0.CR5</version>
+        </dependency>
+        <!-- Database drivers -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>5.1.25</version>
+        </dependency>
+        <dependency>
+            <groupId>postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <version>9.0-801.jdbc4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mongodb</groupId>
+            <artifactId>mongo-java-driver</artifactId>
+            <version>2.11.2</version>
+        </dependency>
+        <!-- Database connection pooling -->
+        <dependency>
+            <groupId>commons-dbcp</groupId>
+            <artifactId>commons-dbcp</artifactId>
+            <version>1.4</version>
+        </dependency>
+        <!-- Caching (and misc. utilities) -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>14.0.1</version>
+        </dependency>
+        <!-- JSON encoding -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+        <!-- HTML templates -->
+        <dependency>
+            <groupId>com.github.spullara.mustache.java</groupId>
+            <artifactId>compiler</artifactId>
+            <version>0.8.13</version>
+        </dependency>
+    </dependencies>
 
 
-  <packaging>jar</packaging>
+    <build>
+        <plugins>
+            <plugin>
+                <inherited>true</inherited>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.3.2</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                    <optimize>true</optimize>
+                    <debug>false</debug>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>hello.HelloWebServer</mainClass>
+                        </manifest>
+                    </archive>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
+                        <phase>package</phase> <!-- bind to the packaging phase -->
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
 
 
-  <dependencies>
-    <dependency>
-    	<groupId>io.undertow</groupId>
-    	<artifactId>undertow-core</artifactId>
-    	<version>1.0.0.Alpha19</version>
-    </dependency>
-	 <dependency>
-		<groupId>com.fasterxml.jackson.core</groupId>
-		<artifactId>jackson-databind</artifactId>
-		<version>2.1.1</version>
-	</dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-			<plugin>
-				<inherited>true</inherited>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-compiler-plugin</artifactId>
-				<version>2.3.2</version>
-				<configuration>
-					<source>1.7</source>
-					<target>1.7</target>
-					<optimize>true</optimize>
-					<debug>false</debug>
-				</configuration>
-			</plugin>
-      <plugin>
-        <artifactId>maven-assembly-plugin</artifactId>
-        <configuration>
-          <archive>
-            <manifest>
-              <mainClass>hello.HelloWebServer</mainClass>
-            </manifest>
-          </archive>
-          <descriptorRefs>
-            <descriptorRef>jar-with-dependencies</descriptorRef>
-          </descriptorRefs>
-        </configuration>
-        <executions>
-          <execution>
-            <id>make-assembly</id> <!-- this is used for inheritance merges -->
-            <phase>package</phase> <!-- bind to the packaging phase -->
-            <goals>
-              <goal>single</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-       </plugins>
-  </build>
-
-    <repositories>
-        <repository>
-            <id>jboss-public-repository-group</id>
-            <name>JBoss Public Maven Repository Group</name>
-            <url>https://repository.jboss.org/nexus/content/groups/public-jboss/</url>
-            <layout>default</layout>
-            <releases>
-                <enabled>true</enabled>
-                <updatePolicy>never</updatePolicy>
-            </releases>
-            <snapshots>
-                <enabled>true</enabled>
-                <updatePolicy>never</updatePolicy>
-            </snapshots>
-        </repository>
-        <repository>
-            <id>jboss-developer-repository-group</id>
-            <name>JBoss Public Maven Repository Group</name>
-            <url>https://repository.jboss.org/nexus/content/groups/developer/</url>
-            <layout>default</layout>
-            <releases>
-                <enabled>true</enabled>
-                <updatePolicy>never</updatePolicy>
-            </releases>
-            <snapshots>
-                <enabled>true</enabled>
-                <updatePolicy>never</updatePolicy>
-            </snapshots>
-        </repository>
-    </repositories>
-</project>
+</project>

+ 1 - 0
undertow/setup.py

@@ -4,6 +4,7 @@ import setup_util
 import os
 import os
 
 
 def start(args):
 def start(args):
+  setup_util.replace_text('undertow/src/main/resources/hello/server.properties', 'DATABASE_HOST', args.database_host)
   try:
   try:
     subprocess.check_call("mvn clean compile assembly:single", shell=True, cwd="undertow")
     subprocess.check_call("mvn clean compile assembly:single", shell=True, cwd="undertow")
     subprocess.Popen("java -jar undertow-example-0.1-jar-with-dependencies.jar".rsplit(" "), cwd="undertow/target")
     subprocess.Popen("java -jar undertow-example-0.1-jar-with-dependencies.jar".rsplit(" "), cwd="undertow/target")

+ 25 - 0
undertow/src/main/java/hello/Fortune.java

@@ -0,0 +1,25 @@
+package hello;
+
+/**
+ * The model for the "fortune" database table.
+ */
+public final class Fortune implements Comparable<Fortune> {
+  public int id;
+  public String message;
+
+  /**
+   * Constructs a new fortune object with the given parameters.
+   *
+   * @param id the ID of the fortune
+   * @param message the message of the fortune
+   */
+  public Fortune(int id, String message) {
+    this.id = id;
+    this.message = message;
+  }
+
+  @Override
+  public int compareTo(Fortune other) {
+    return message.compareTo(other.message);
+  }
+}

+ 520 - 45
undertow/src/main/java/hello/HelloWebServer.java

@@ -1,64 +1,539 @@
 package hello;
 package hello;
 
 
-import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.Map;
-
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.ObjectWriter;
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.net.MediaType;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DB;
+import com.mongodb.DBCursor;
+import com.mongodb.DBObject;
+import com.mongodb.MongoClient;
+import io.undertow.Handlers;
 import io.undertow.Undertow;
 import io.undertow.Undertow;
 import io.undertow.server.HttpHandler;
 import io.undertow.server.HttpHandler;
 import io.undertow.server.HttpServerExchange;
 import io.undertow.server.HttpServerExchange;
 import io.undertow.util.Headers;
 import io.undertow.util.Headers;
+import org.apache.commons.dbcp.ConnectionFactory;
+import org.apache.commons.dbcp.DriverManagerConnectionFactory;
+import org.apache.commons.dbcp.PoolableConnectionFactory;
+import org.apache.commons.dbcp.PoolingDataSource;
+import org.apache.commons.pool.impl.GenericObjectPool;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * An implementation of the TechEmpower benchmark tests using the Undertow web
+ * server.  The only test that truly exercises Undertow in isolation is the
+ * plaintext test.  For the rest, it uses best-of-breed components that are
+ * expected to perform well.  The idea is that using these components enables
+ * these tests to serve as performance baselines for hypothetical, Undertow-based
+ * frameworks.  For instance, it is unlikely that such frameworks would complete
+ * the JSON test faster than this will, because this implementation uses
+ * Undertow and Jackson in the most direct way possible to fulfill the test
+ * requirements.
+ */
+public final class HelloWebServer {
+
+  public static void main(String[] args) throws Exception {
+    new HelloWebServer();
+  }
+
+  // Helper methods
+
+  /**
+   * Constructs a new SQL data source with the given parameters.  Connections
+   * to this data source are pooled.
+   *
+   * @param uri the URI for database connections
+   * @param user the username for the database
+   * @param password the password for the database
+   * @return a new SQL data source
+   */
+  private static DataSource newDataSource(String uri,
+                                          String user,
+                                          String password) {
+    GenericObjectPool connectionPool = new GenericObjectPool();
+    connectionPool.setMaxActive(256);
+    connectionPool.setMaxIdle(256);
+    ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(
+        uri, user, password);
+    //
+    // This constructor modifies the connection pool, setting its connection
+    // factory to this.  (So despite how it may appear, all of the objects
+    // declared in this method are incorporated into the returned result.)
+    //
+    new PoolableConnectionFactory(
+        connectionFactory, connectionPool, null, null, false, true);
+    return new PoolingDataSource(connectionPool);
+  }
+
+  /**
+   * Returns the value of the "queries" request parameter, which is an integer
+   * bound between 1 and 500 with a default value of 1.
+   *
+   * @param exchange the current HTTP exchange
+   * @return the value of the "queries" request parameter
+   */
+  private static int getQueries(HttpServerExchange exchange) {
+    Deque<String> values = exchange.getQueryParameters().get("queries");
+    if (values == null) {
+      return 1;
+    }
+    String textValue = values.peekFirst();
+    if (textValue == null) {
+      return 1;
+    }
+    try {
+      int parsedValue = Integer.parseInt(textValue);
+      return Math.min(500, Math.max(1, parsedValue));
+    } catch (NumberFormatException e) {
+      return 1;
+    }
+  }
+
+  /**
+   * Returns a random integer that is a suitable value for both the {@code id}
+   * and {@code randomNumber} properties of a world object.
+   *
+   * @return a random world number
+   */
+  private static int randomWorld() {
+    return 1 + ThreadLocalRandom.current().nextInt(10000);
+  }
 
 
-import static io.undertow.Undertow.builder;
+  // Fields and constructor
 
 
-public class HelloWebServer {
+  private final ObjectMapper objectMapper = new ObjectMapper();
+  private final MustacheFactory mustacheFactory = new DefaultMustacheFactory();
+  private final DataSource mysql;
+  private final DataSource postgresql;
+  private final DB mongodb;
+  private final LoadingCache<Integer, World> worldCache;
 
 
-    private static final ObjectWriter writer = new ObjectMapper().writerWithType(Map.class);
+  /**
+   * Creates and starts a new web server whose configuration is specified in the
+   * {@code server.properties} file.
+   *
+   * @throws IOException if the application properties file cannot be read or
+   *                     the Mongo database hostname cannot be resolved
+   * @throws SQLException if reading from the SQL database (while priming the
+   *                      cache) fails
+   */
+  public HelloWebServer() throws IOException, SQLException {
+    Properties properties = new Properties();
+    try (InputStream in = getClass().getResourceAsStream("server.properties")) {
+      properties.load(in);
+    }
+    mysql = newDataSource(
+        properties.getProperty("mysql.uri"),
+        properties.getProperty("mysql.user"),
+        properties.getProperty("mysql.password"));
+    postgresql = newDataSource(
+        properties.getProperty("postgresql.uri"),
+        properties.getProperty("postgresql.user"),
+        properties.getProperty("postgresql.password"));
+    mongodb = new MongoClient(properties.getProperty("mongodb.host"))
+        .getDB(properties.getProperty("mongodb.name"));
+    //
+    // The world cache is primed at startup with all values.  It doesn't
+    // matter which database backs it; they all contain the same information
+    // and the CacheLoader.load implementation below is never invoked.
+    //
+    worldCache = CacheBuilder.newBuilder()
+        .build(new CacheLoader<Integer, World>() {
+          @Override
+          public World load(Integer id) throws Exception {
+            try (Connection connection = mysql.getConnection();
+                 PreparedStatement statement = connection.prepareStatement(
+                     "SELECT * FROM World WHERE id = ?",
+                     ResultSet.TYPE_FORWARD_ONLY,
+                     ResultSet.CONCUR_READ_ONLY)) {
+              statement.setInt(1, id);
+              try (ResultSet resultSet = statement.executeQuery()) {
+                resultSet.next();
+                return new World(
+                    resultSet.getInt("id"),
+                    resultSet.getInt("randomNumber"));
+              }
+            }
+          }
+        });
+    try (Connection connection = mysql.getConnection();
+         PreparedStatement statement = connection.prepareStatement(
+             "SELECT * FROM World",
+             ResultSet.TYPE_FORWARD_ONLY,
+             ResultSet.CONCUR_READ_ONLY);
+         ResultSet resultSet = statement.executeQuery()) {
+      while (resultSet.next()) {
+        World world = new World(
+            resultSet.getInt("id"),
+            resultSet.getInt("randomNumber"));
+        worldCache.put(world.id, world);
+      }
+    }
+    Undertow.builder()
+        .addListener(
+            Integer.parseInt(properties.getProperty("web.port")),
+            properties.getProperty("web.host"))
+        .setBufferSize(1024 * 16)
+        //
+        // TODO: Figure out the best values for IoThreads and WorkerThreads.
+        //
+        // It is probably some function of the number of processor cores and the
+        // expected client-side concurrency.  The values below seemed to do the
+        // best out of several that we tried in spot tests on our 8-core i7
+        // hardware.
+        //
+        .setIoThreads(64)
+        .setWorkerThreads(256)
+        .setHandler(Handlers.date(Handlers.header(Handlers.path()
+            .addPath("/json", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleJsonTest(exchange);
+              }
+            })
+            .addPath("/db/mysql", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleDbTest(exchange, mysql);
+              }
+            })
+            .addPath("/db/postgresql", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleDbTest(exchange, postgresql);
+              }
+            })
+            .addPath("/db/mongodb", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleDbTest(exchange, mongodb);
+              }
+            })
+            .addPath("/fortunes/mysql", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleFortunesTest(exchange, mysql);
+              }
+            })
+            .addPath("/fortunes/postgresql", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleFortunesTest(exchange, postgresql);
+              }
+            })
+            .addPath("/fortunes/mongodb", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleFortunesTest(exchange, mongodb);
+              }
+            })
+            .addPath("/updates/mysql", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleUpdatesTest(exchange, mysql);
+              }
+            })
+            .addPath("/updates/postgresql", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleUpdatesTest(exchange, postgresql);
+              }
+            })
+            .addPath("/updates/mongodb", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleUpdatesTest(exchange, mongodb);
+              }
+            })
+            .addPath("/plaintext", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handlePlaintextTest(exchange);
+              }
+            })
+            .addPath("/cache", new HttpHandler() {
+              @Override
+              public void handleRequest(HttpServerExchange exchange)
+                  throws Exception {
+                handleCacheTest(exchange);
+              }
+            }), Headers.SERVER_STRING, "undertow")))
+        .build()
+        .start();
+  }
 
 
-    public static final String HELLO_WORLD = "Hello, World!";
+  // Request handlers
 
 
-    private final int port;
-    private final ByteBuffer buffer;
+  /**
+   * Completes the JSON test for the given request.
+   *
+   * @param exchange the current HTTP exchange
+   * @throws IOException if JSON encoding fails
+   */
+  private void handleJsonTest(HttpServerExchange exchange) throws IOException {
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(
+        objectMapper.writeValueAsString(
+            Collections.singletonMap("message", "Hello, world")));
+  }
+
+  /**
+   * Completes the database query test for the given request using a SQL
+   * database.  This handler is used for both the single-query and
+   * multiple-query tests.
+   *
+   * @param exchange the current HTTP exchange
+   * @param database the SQL database
+   * @throws IOException if JSON encoding fails
+   * @throws SQLException if reading from the database fails
+   */
+  private void handleDbTest(HttpServerExchange exchange, DataSource database)
+      throws IOException, SQLException {
+    int queries = getQueries(exchange);
+    World[] worlds = new World[queries];
+    try (Connection connection = database.getConnection();
+         PreparedStatement statement = connection.prepareStatement(
+             "SELECT * FROM World WHERE id = ?",
+             ResultSet.TYPE_FORWARD_ONLY,
+             ResultSet.CONCUR_READ_ONLY)) {
+      for (int i = 0; i < queries; i++) {
+        statement.setInt(1, randomWorld());
+        try (ResultSet resultSet = statement.executeQuery()) {
+          resultSet.next();
+          worlds[i] = new World(
+              resultSet.getInt("id"),
+              resultSet.getInt("randomNumber"));
+        }
+      }
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+
+  /**
+   * Completes the database query test for the given request using a Mongo
+   * database.  This handler is used for both the single-query and
+   * multiple-query tests.
+   *
+   * @param exchange the current HTTP exchange
+   * @param database the Mongo database
+   * @throws IOException if JSON encoding fails
+   */
+  private void handleDbTest(HttpServerExchange exchange, DB database)
+      throws IOException {
+    int queries = getQueries(exchange);
+    World[] worlds = new World[queries];
+    for (int i = 0; i < queries; i++) {
+      DBObject object = database.getCollection("world").findOne(
+          new BasicDBObject("id", randomWorld()));
+      worlds[i] = new World(
+          //
+          // The creation script for the Mongo database inserts these numbers as
+          // JavaScript numbers, which resolve to doubles in Java.
+          //
+          ((Number) object.get("id")).intValue(),
+          ((Number) object.get("randomNumber")).intValue());
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
 
 
-    public HelloWebServer(int port) {
-        this.port = port;
-        buffer = ByteBuffer.allocateDirect(HELLO_WORLD.getBytes().length);
-        buffer.put(HELLO_WORLD.getBytes());
-        buffer.flip();
+  /**
+   * Completes the fortunes test for the given request using a SQL database.
+   *
+   * @param exchange the current HTTP exchange
+   * @param database the SQL database
+   * @throws SQLException if reading from the database fails
+   */
+  private void handleFortunesTest(HttpServerExchange exchange,
+                                  DataSource database)
+      throws SQLException {
+    List<Fortune> fortunes = new ArrayList<>();
+    try (Connection connection = database.getConnection();
+         PreparedStatement statement = connection.prepareStatement(
+             "SELECT * FROM Fortune",
+             ResultSet.TYPE_FORWARD_ONLY,
+             ResultSet.CONCUR_READ_ONLY);
+         ResultSet resultSet = statement.executeQuery()) {
+      while (resultSet.next()) {
+        fortunes.add(new Fortune(
+            resultSet.getInt("id"),
+            resultSet.getString("message")));
+      }
     }
     }
+    fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+    Collections.sort(fortunes);
+    Mustache mustache = mustacheFactory.compile("hello/fortunes.mustache");
+    StringWriter writer = new StringWriter();
+    mustache.execute(writer, fortunes);
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.HTML_UTF_8.toString());
+    exchange.getResponseSender().send(writer.toString());
+  }
 
 
-    public void run() throws Exception {
-
-        Undertow undertow = builder()
-                .addListener(port, "0.0.0.0")
-                .setBufferSize(1024 * 16)
-                .setHandler(new HttpHandler() {
-                    @Override
-                    public void handleRequest(final HttpServerExchange exchange) throws Exception {
-                        if (exchange.getRelativePath().equals("/plaintext")) {
-                            exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
-                            exchange.getResponseSender().send(buffer.duplicate());
-                        } else {
-                            exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
-                            Map<String, String> data = Collections.singletonMap("message", "Hello, world");
-                            String response = writer.writeValueAsString(data);
-                            exchange.getResponseSender().send(response);
-                        }
-                    }
-                }).build();
-
-        undertow.start();
+  /**
+   * Completes the fortunes test for the given request using a Mongo database.
+   *
+   * @param exchange the current HTTP exchange
+   * @param database the Mongo database
+   */
+  private void handleFortunesTest(HttpServerExchange exchange, DB database) {
+    List<Fortune> fortunes = new ArrayList<>();
+    DBCursor cursor = database.getCollection("fortune").find();
+    while (cursor.hasNext()) {
+      DBObject object = cursor.next();
+      fortunes.add(new Fortune(
+          ((Number) object.get("id")).intValue(),
+          (String) object.get("message")));
     }
     }
+    fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+    Collections.sort(fortunes);
+    Mustache mustache = mustacheFactory.compile("hello/fortunes.mustache");
+    StringWriter writer = new StringWriter();
+    mustache.execute(writer, fortunes);
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.HTML_UTF_8.toString());
+    exchange.getResponseSender().send(writer.toString());
+  }
 
 
-    public static void main(String[] args) throws Exception {
-        int port;
-        if (args.length > 0) {
-            port = Integer.parseInt(args[0]);
-        } else {
-            port = 8080;
+  /**
+   * Completes the database update test for the given request using a SQL
+   * database.
+   *
+   * @param exchange the current HTTP exchange
+   * @param database the SQL database
+   * @throws IOException if JSON encoding fails
+   * @throws SQLException if reading from or writing to the database fails
+   */
+  private void handleUpdatesTest(HttpServerExchange exchange,
+                                 DataSource database)
+      throws IOException, SQLException {
+    int queries = getQueries(exchange);
+    World[] worlds = new World[queries];
+    try (Connection connection = database.getConnection();
+         PreparedStatement query = connection.prepareStatement(
+             "SELECT * FROM World WHERE id = ?",
+             ResultSet.TYPE_FORWARD_ONLY,
+             ResultSet.CONCUR_READ_ONLY);
+         PreparedStatement update = connection.prepareStatement(
+             "UPDATE World SET randomNumber = ? WHERE id= ?")) {
+      for (int i = 0; i < queries; i++) {
+        query.setInt(1, randomWorld());
+        World world;
+        try (ResultSet resultSet = query.executeQuery()) {
+          resultSet.next();
+          world = new World(
+              resultSet.getInt("id"),
+              resultSet.getInt("randomNumber"));
         }
         }
-        new HelloWebServer(port).run();
+        world.randomNumber = randomWorld();
+        update.setInt(1, world.randomNumber);
+        update.setInt(2, world.id);
+        update.executeUpdate();
+        worlds[i] = world;
+      }
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+
+  /**
+   * Completes the database update test for the given request using a Mongo
+   * database.
+   *
+   * @param exchange the current HTTP exchange
+   * @param database the Mongo database
+   * @throws IOException if JSON encoding fails
+   */
+  private void handleUpdatesTest(HttpServerExchange exchange, DB database)
+      throws IOException {
+    int queries = getQueries(exchange);
+    World[] worlds = new World[queries];
+    for (int i = 0; i < queries; i++) {
+      int id = randomWorld();
+      DBObject key = new BasicDBObject("id", id);
+      //
+      // The requirements for the test dictate that we must fetch the World
+      // object from the data store and read its randomNumber field, even though
+      // we could technically avoid doing either of those things and still
+      // produce the correct output and side effects.
+      //
+      DBObject object = database.getCollection("world").findOne(key);
+      int oldRandomNumber = ((Number) object.get("randomNumber")).intValue();
+      int newRandomNumber = randomWorld();
+      object.put("randomNumber", newRandomNumber);
+      database.getCollection("world").update(key, object);
+      worlds[i] = new World(id, newRandomNumber);
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+
+  /**
+   * Completes the plaintext test forthe given request.
+   *
+   * @param exchange the current HTTP exchange
+   */
+  private void handlePlaintextTest(HttpServerExchange exchange) {
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString());
+    exchange.getResponseSender().send("Hello, world");
+  }
+
+  /**
+   * Completes the cache test for the given request.
+   *
+   * @param exchange the current HTTP exchange
+   * @throws ExecutionException the reading from the cache fails
+   * @throws IOException if JSON encoding fails
+   */
+
+  private void handleCacheTest(HttpServerExchange exchange)
+      throws ExecutionException, IOException {
+    int queries = getQueries(exchange);
+    World[] worlds = new World[queries];
+    for (int i = 0; i < queries; i++) {
+      worlds[i] = worldCache.get(randomWorld());
     }
     }
-}
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 20 - 0
undertow/src/main/java/hello/World.java

@@ -0,0 +1,20 @@
+package hello;
+
+/**
+ * The model for the "world" database table.
+ */
+public final class World {
+  public int id;
+  public int randomNumber;
+
+  /**
+   * Constructs a new world object with the given parameters.
+   *
+   * @param id the ID of the world
+   * @param randomNumber the random number of the world
+   */
+  public World(int id, int randomNumber) {
+    this.id = id;
+    this.randomNumber = randomNumber;
+  }
+}

+ 20 - 0
undertow/src/main/resources/hello/fortunes.mustache

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Fortunes</title>
+</head>
+<body>
+<table>
+    <tr>
+        <th>id</th>
+        <th>message</th>
+    </tr>
+    {{#.}}
+      <tr>
+          <td>{{id}}</td>
+          <td>{{message}}</td>
+      </tr>
+    {{/.}}
+</table>
+</body>
+</html>

+ 14 - 0
undertow/src/main/resources/hello/server.properties

@@ -0,0 +1,14 @@
+web.port = 8080
+web.host = 0.0.0.0
+mysql.uri = jdbc:mysql://DATABASE_HOST:3306/hello_world?jdbcCompliantTruncation=false&elideSetAutoCommits=true&useLocalSessionState=true&cachePrepStmts=true&cacheCallableStmts=true&alwaysSendSetIsolation=false&prepStmtCacheSize=4096&cacheServerConfiguration=true&prepStmtCacheSqlLimit=2048&zeroDateTimeBehavior=convertToNull&traceProtocol=false&useUnbufferedInput=false&useReadAheadInput=false&maintainTimeStats=false&useServerPrepStmts&cacheRSMetadata=true
+mysql.user = benchmarkdbuser
+mysql.password = benchmarkdbpass
+#
+# TODO: Audit this postgresql connection string.
+# It looks copy & pasted from MySQL.  Do these parameters even do anything?
+#
+postgresql.uri = jdbc:postgresql://DATABASE_HOST:5432/hello_world?jdbcCompliantTruncation=false&elideSetAutoCommits=true&useLocalSessionState=true&cachePrepStmts=true&cacheCallableStmts=true&alwaysSendSetIsolation=false&prepStmtCacheSize=4096&cacheServerConfiguration=true&prepStmtCacheSqlLimit=2048&zeroDateTimeBehavior=convertToNull&traceProtocol=false&useUnbufferedInput=false&useReadAheadInput=false&maintainTimeStats=false&useServerPrepStmts&cacheRSMetadata=true
+postgresql.user = benchmarkdbuser
+postgresql.password = benchmarkdbpass
+mongodb.host = DATABASE_HOST:27017
+mongodb.name = hello_world