Browse Source

Merge branch 'edge' of https://github.com/ctomc/FrameworkBenchmarks into PR738

Mike Smith 11 years ago
parent
commit
117a4641fd

+ 17 - 0
undertow-edge/README.md

@@ -0,0 +1,17 @@
+# Undertow Edge Benchmarking Test
+
+This is the undertow portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+Edge version of undertow framework uses native implementation of xnio instead of NIO
+
+### JSON Encoding Test
+* [JSON test source](src/main/java/hello/HelloWebServer.java)
+
+## Versions
+Undertow 1.0.0.Beta31 (http://undertow.io)
+
+## Test URLs
+
+### JSON Encoding Test
+
+    http://localhost:8080

+ 0 - 0
undertow-edge/__init__.py


+ 88 - 0
undertow-edge/benchmark_config

@@ -0,0 +1,88 @@
+{
+  "framework": "undertow-edge",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "cache_url": "/cache",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "None",
+      "framework": "undertow-edge",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "Undertow Edge",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "undertow edge",
+      "notes": "",
+      "versus": ""
+    },
+    "mysql" : {
+      "setup_file": "setup",
+      "db_url": "/db/mysql",
+      "query_url": "/queries/mysql?queries=",
+      "fortune_url": "/fortunes/mysql",
+      "update_url": "/updates/mysql?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "MySQL",
+      "framework": "undertow-edge",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "Undertow Edge",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "undertow edge",
+      "notes": "",
+      "versus": ""
+    },
+    "postgresql" : {
+      "setup_file": "setup",
+      "db_url": "/db/postgresql",
+      "query_url": "/queries/postgresql?queries=",
+      "fortune_url": "/fortunes/postgresql",
+      "update_url": "/updates/postgresql?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "Postgres",
+      "framework": "undertow-edge",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "Undertow Edge",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "undertow edge",
+      "notes": "",
+      "versus": ""
+    },
+    "mongodb" : {
+      "setup_file": "setup",
+      "db_url": "/db/mongodb",
+      "query_url": "/queries/mongodb?queries=",
+      "fortune_url": "/fortunes/mongodb",
+      "update_url": "/updates/mongodb?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "MongoDB",
+      "framework": "undertow-edge",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "Undertow Edge",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "undertow edge",
+      "notes": "",
+      "versus": ""
+    }
+  }]
+}

+ 215 - 0
undertow-edge/pom.xml

@@ -0,0 +1,215 @@
+<?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>
+    <parent>
+        <groupId>org.jboss</groupId>
+        <artifactId>jboss-parent</artifactId>
+        <version>12</version>
+    </parent>
+
+    <groupId>com.techempower</groupId>
+    <artifactId>undertow-edge</artifactId>
+    <version>0.1</version>
+
+    <dependencies>
+        <!-- Web server -->
+        <dependency>
+            <groupId>io.undertow</groupId>
+            <artifactId>undertow-core</artifactId>
+            <version>1.0.0.Beta31</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.jboss.xnio</groupId>
+                    <artifactId>xnio-nio</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.xnio</groupId>
+            <artifactId>xnio-api</artifactId>
+            <version>3.2.0.Beta4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.xnio</groupId>
+            <artifactId>xnio-native</artifactId>
+            <version>1.0.0.Beta1</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.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>2.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>2.3.0</version>
+        </dependency>
+        <!-- HTML templates -->
+        <dependency>
+            <groupId>com.github.spullara.mustache.java</groupId>
+            <artifactId>compiler</artifactId>
+            <version>0.8.13</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <inherited>true</inherited>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                    <optimize>true</optimize>
+                    <debug>false</debug>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>com.googlecode.maven-download-plugin</groupId>
+                <artifactId>maven-download-plugin</artifactId>
+                <version>1.1.0</version>
+                <executions>
+                    <execution>
+                        <id>install-xnio-native</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>wget</goal>
+                        </goals>
+                        <configuration>
+                            <url>http://downloads.jboss.org/xnio/native/1.0/1.0.0.Beta1/xnio-native-1.0.0.Beta1.zip</url>
+                            <unpack>true</unpack>
+                            <outputDirectory>${project.build.directory}</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <configuration>
+
+                            <target>
+                                <copy todir="${project.build.directory}" file="${project.build.directory}/${native.path}"/>
+                            </target>
+
+                        </configuration>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>hello.HelloWebServer</mainClass>
+                        </manifest>
+                    </archive>
+                    <attach>
+                    </attach>
+                    <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>
+    <profiles>
+        <profile>
+            <id>32bit-native</id>
+            <activation>
+                <os>
+                    <arch>x86</arch>
+                </os>
+            </activation>
+            <properties>
+                <native.path>linux-i686/libxnio.so</native.path>
+            </properties>
+        </profile>
+
+        <profile>
+            <id>64bit-native</id>
+            <activation>
+                <os>
+                    <arch>amd64</arch>
+                </os>
+            </activation>
+            <properties>
+                <native.path>linux-x86_64/libxnio.so</native.path>
+            </properties>
+        </profile>
+    </profiles>
+    <repositories>
+        <repository>
+            <id>jboss-public-repository-group</id>
+            <name>JBoss Public Maven Repository Group</name>
+            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
+            <layout>default</layout>
+            <releases>
+                <enabled>true</enabled>
+                <updatePolicy>never</updatePolicy>
+            </releases>
+            <snapshots>
+                <enabled>true</enabled>
+                <updatePolicy>never</updatePolicy>
+            </snapshots>
+        </repository>
+    </repositories>
+
+</project>

+ 21 - 0
undertow-edge/setup.py

@@ -0,0 +1,21 @@
+import subprocess
+import sys
+import setup_util
+import os
+
+def start(args, logfile, errfile):
+  setup_util.replace_text('undertow-edge/src/main/resources/hello/server.properties', 'DATABASE_HOST', args.database_host)
+  try:
+    subprocess.check_call("mvn clean package", shell=True, cwd="undertow-edge", stderr=errfile, stdout=logfile)
+    subprocess.Popen("java -Djava.library.path=target/ -jar target/undertow-edge-0.1-jar-with-dependencies.jar".rsplit(" "), cwd="undertow-edge", stderr=errfile, stdout=logfile)
+    return 0
+  except subprocess.CalledProcessError:
+    return 1
+def stop(logfile, errfile):
+  p = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE)
+  out, err = p.communicate()
+  for line in out.splitlines():
+    if 'undertow-edge-0.1-jar-with-dependencies.jar' in line:
+      pid = int(line.split(None, 2)[1])
+      os.kill(pid, 9)
+  return 0

+ 4 - 0
undertow-edge/source_code

@@ -0,0 +1,4 @@
+./undertow/src/main/java/hello/
+./undertow/src/main/java/hello/HelloWebServer.java
+./undertow/src/main/java/hello/World.java
+./undertow/src/main/java/hello/Fortune.java

+ 37 - 0
undertow-edge/src/main/java/hello/CacheHandler.java

@@ -0,0 +1,37 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.cache.LoadingCache;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import java.util.Objects;
+
+import static hello.HelloWebServer.JSON_UTF8;
+
+/**
+ * Handles the cache access test.
+ */
+final class CacheHandler implements HttpHandler {
+  private final ObjectMapper objectMapper;
+  private final LoadingCache<Integer, World> worldCache;
+
+  CacheHandler(ObjectMapper objectMapper,
+               LoadingCache<Integer, World> worldCache) {
+    this.objectMapper = Objects.requireNonNull(objectMapper);
+    this.worldCache = Objects.requireNonNull(worldCache);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    int queries = Helper.getQueries(exchange);
+    World[] worlds = new World[queries];
+    for (int i = 0; i < queries; i++) {
+      worlds[i] = worldCache.get(Helper.randomWorld());
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, JSON_UTF8);
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 68 - 0
undertow-edge/src/main/java/hello/DbMongoHandler.java

@@ -0,0 +1,68 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DB;
+import com.mongodb.DBObject;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import java.util.Objects;
+
+import static hello.HelloWebServer.JSON_UTF8;
+
+/**
+ * Handles the single- and multiple-query database tests using MongoDB.
+ */
+final class DbMongoHandler implements HttpHandler {
+  private final ObjectMapper objectMapper;
+  private final DB database;
+  private final boolean multiple;
+
+  DbMongoHandler(ObjectMapper objectMapper, DB database, boolean multiple) {
+    this.objectMapper = Objects.requireNonNull(objectMapper);
+    this.database = Objects.requireNonNull(database);
+    this.multiple = multiple;
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    if (exchange.isInIoThread()) {
+      exchange.dispatch(this);
+      return;
+    }
+    
+    int queries = 1;
+    if(multiple)
+    {
+      queries = Helper.getQueries(exchange);
+    }
+    
+    World[] worlds = new World[queries];
+    for (int i = 0; i < queries; i++) {
+      DBObject object = database.getCollection("World").findOne(
+          new BasicDBObject("_id", Helper.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, JSON_UTF8);
+    
+    if (multiple)
+    {
+      // If a multiple query then response must be an array
+      exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+    }
+    else
+    {
+      // If a single query then response must be an object
+      exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds[0]));
+    }
+  }
+}

+ 75 - 0
undertow-edge/src/main/java/hello/DbSqlHandler.java

@@ -0,0 +1,75 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import javax.sql.DataSource;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.Objects;
+
+import static hello.HelloWebServer.JSON_UTF8;
+
+/**
+ * Handles the single- and multiple-query database tests using a SQL database.
+ */
+final class DbSqlHandler implements HttpHandler {
+  private final ObjectMapper objectMapper;
+  private final DataSource database;
+  private final boolean multiple;
+
+  DbSqlHandler(ObjectMapper objectMapper, DataSource database, boolean multiple) {
+    this.objectMapper = Objects.requireNonNull(objectMapper);
+    this.database = Objects.requireNonNull(database);
+    this.multiple = multiple;
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    if (exchange.isInIoThread()) {
+      exchange.dispatch(this);
+      return;
+    }
+    
+    int queries = 1;
+    if(multiple)
+    {
+      queries = Helper.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, Helper.randomWorld());
+        try (ResultSet resultSet = statement.executeQuery()) {
+          resultSet.next();
+          worlds[i] = new World(
+              resultSet.getInt("id"),
+              resultSet.getInt("randomNumber"));
+        }
+      }
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, JSON_UTF8);
+    
+    if (multiple)
+    {
+      // If a multiple query then response must be an array
+      exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+    }
+    else
+    {
+      // If a single query then response must be an object
+      exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds[0]));
+    }
+  }
+}

+ 25 - 0
undertow-edge/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);
+  }
+}

+ 55 - 0
undertow-edge/src/main/java/hello/FortunesMongoHandler.java

@@ -0,0 +1,55 @@
+package hello;
+
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import com.mongodb.DB;
+import com.mongodb.DBCursor;
+import com.mongodb.DBObject;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static hello.HelloWebServer.HTML_UTF8;
+
+/**
+ * Handles the fortunes test using MongoDB.
+ */
+final class FortunesMongoHandler implements HttpHandler {
+  private final MustacheFactory mustacheFactory;
+  private final DB database;
+
+  FortunesMongoHandler(MustacheFactory mustacheFactory, DB database) {
+    this.mustacheFactory = Objects.requireNonNull(mustacheFactory);
+    this.database = Objects.requireNonNull(database);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    if (exchange.isInIoThread()) {
+      exchange.dispatch(this);
+      return;
+    }
+    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, HTML_UTF8);
+    exchange.getResponseSender().send(writer.toString());
+  }
+}

+ 61 - 0
undertow-edge/src/main/java/hello/FortunesSqlHandler.java

@@ -0,0 +1,61 @@
+package hello;
+
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import javax.sql.DataSource;
+import java.io.StringWriter;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static hello.HelloWebServer.HTML_UTF8;
+
+/**
+ * Handles the fortunes test using a SQL database.
+ */
+final class FortunesSqlHandler implements HttpHandler {
+  private final MustacheFactory mustacheFactory;
+  private final DataSource database;
+
+  FortunesSqlHandler(MustacheFactory mustacheFactory, DataSource database) {
+    this.mustacheFactory = Objects.requireNonNull(mustacheFactory);
+    this.database = Objects.requireNonNull(database);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    if (exchange.isInIoThread()) {
+      exchange.dispatch(this);
+      return;
+    }
+    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, HTML_UTF8);
+    exchange.getResponseSender().send(writer.toString());
+  }
+}

+ 159 - 0
undertow-edge/src/main/java/hello/HelloWebServer.java

@@ -0,0 +1,159 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.mustachejava.DefaultMustacheFactory;
+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.DB;
+import com.mongodb.MongoClient;
+import io.undertow.Handlers;
+import io.undertow.Undertow;
+import io.undertow.UndertowOptions;
+import io.undertow.util.Headers;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Properties;
+
+/**
+ * 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 {
+
+  //MediaType.toString() does non-trivial work and does not cache the result
+  //so we cache it here
+  public static final String JSON_UTF8 = MediaType.JSON_UTF_8.toString();
+
+  public static final String TEXT_PLAIN = MediaType.PLAIN_TEXT_UTF_8.toString();
+
+  public static final String HTML_UTF8 = MediaType.HTML_UTF_8.toString();
+
+  public static void main(String[] args) throws Exception {
+    new HelloWebServer();
+  }
+
+  /**
+   * 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 ClassNotFoundException, IOException, SQLException {
+    Class.forName("org.postgresql.Driver");
+    Properties properties = new Properties();
+    try (InputStream in = HelloWebServer.class.getResourceAsStream(
+        "server.properties")) {
+      properties.load(in);
+    }
+    final ObjectMapper objectMapper = new ObjectMapper();
+    final MustacheFactory mustacheFactory = new DefaultMustacheFactory();
+    final DataSource mysql = Helper.newDataSource(
+        properties.getProperty("mysql.uri"),
+        properties.getProperty("mysql.user"),
+        properties.getProperty("mysql.password"));
+    final DataSource postgresql = Helper.newDataSource(
+        properties.getProperty("postgresql.uri"),
+        properties.getProperty("postgresql.user"),
+        properties.getProperty("postgresql.password"));
+    final DB 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.
+    //
+    final LoadingCache<Integer, World> 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)
+        .setIoThreads(Runtime.getRuntime().availableProcessors() * 2) //this seems slightly faster in some configurations
+        .setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false) //don't send a keep-alive header for HTTP/1.1 requests, as it is not required
+        .setHandler(Handlers.date(Handlers.header(Handlers.path()
+            .addPrefixPath("/json",
+                new JsonHandler(objectMapper))
+            .addPrefixPath("/db/mysql",
+                new DbSqlHandler(objectMapper, mysql, false))
+            .addPrefixPath("/queries/mysql",
+                new DbSqlHandler(objectMapper, mysql, true))
+            .addPrefixPath("/db/postgresql",
+                new DbSqlHandler(objectMapper, postgresql, false))
+            .addPrefixPath("/queries/postgresql",
+                new DbSqlHandler(objectMapper, postgresql, true))
+            .addPrefixPath("/db/mongodb",
+                new DbMongoHandler(objectMapper, mongodb, false))
+            .addPrefixPath("/queries/mongodb",
+                new DbMongoHandler(objectMapper, mongodb, true))
+            .addPrefixPath("/fortunes/mysql",
+                new FortunesSqlHandler(mustacheFactory, mysql))
+            .addPrefixPath("/fortunes/postgresql",
+                new FortunesSqlHandler(mustacheFactory, postgresql))
+            .addPrefixPath("/fortunes/mongodb",
+                new FortunesMongoHandler(mustacheFactory, mongodb))
+            .addPrefixPath("/updates/mysql",
+                new UpdatesSqlHandler(objectMapper, mysql))
+            .addPrefixPath("/updates/postgresql",
+                new UpdatesSqlHandler(objectMapper, postgresql))
+            .addPrefixPath("/updates/mongodb",
+                new UpdatesMongoHandler(objectMapper, mongodb))
+            .addPrefixPath("/plaintext",
+                new PlaintextHandler())
+            .addPrefixPath("/cache",
+                new CacheHandler(objectMapper, worldCache)),
+            Headers.SERVER_STRING, "U-tow")))
+        .setWorkerThreads(200)
+        .build()
+        .start();
+  }
+}

+ 82 - 0
undertow-edge/src/main/java/hello/Helper.java

@@ -0,0 +1,82 @@
+package hello;
+
+import io.undertow.server.HttpServerExchange;
+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.util.Deque;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Provides utility methods for the benchmark tests.
+ */
+final class Helper {
+  private Helper() {
+    throw new AssertionError();
+  }
+
+  /**
+   * 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
+   */
+  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
+   */
+  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
+   */
+  static int randomWorld() {
+    return 1 + ThreadLocalRandom.current().nextInt(10000);
+  }
+}

+ 32 - 0
undertow-edge/src/main/java/hello/JsonHandler.java

@@ -0,0 +1,32 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Objects;
+
+import static hello.HelloWebServer.JSON_UTF8;
+
+/**
+ * Handles the JSON test.
+ */
+final class JsonHandler implements HttpHandler {
+  private final ObjectMapper objectMapper;
+
+  public JsonHandler(ObjectMapper objectMapper) {
+    this.objectMapper = Objects.requireNonNull(objectMapper);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, JSON_UTF8);
+    exchange.getResponseSender().send(ByteBuffer.wrap(
+            objectMapper.writeValueAsBytes(
+                    Collections.singletonMap("message", "Hello, World!"))));
+  }
+}

+ 33 - 0
undertow-edge/src/main/java/hello/PlaintextHandler.java

@@ -0,0 +1,33 @@
+package hello;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+import java.nio.ByteBuffer;
+
+import static hello.HelloWebServer.TEXT_PLAIN;
+
+/**
+ * Handles the plaintext test.
+ */
+final class PlaintextHandler implements HttpHandler {
+  private static final ByteBuffer buffer;
+  private static final String MESSAGE = "Hello, World!";
+
+  static {
+      buffer = ByteBuffer.allocateDirect(MESSAGE.length());   
+      try {
+          buffer.put(MESSAGE.getBytes("US-ASCII"));
+      } catch (Exception e) {
+          throw new RuntimeException(e);
+      }
+      buffer.flip();
+  }
+     
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, TEXT_PLAIN);
+    exchange.getResponseSender().send(buffer.duplicate());
+  }
+}

+ 60 - 0
undertow-edge/src/main/java/hello/UpdatesMongoHandler.java

@@ -0,0 +1,60 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DB;
+import com.mongodb.DBObject;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import java.util.Objects;
+
+import static hello.HelloWebServer.JSON_UTF8;
+
+/**
+ * Handles the updates test using MongoDB.
+ */
+final class UpdatesMongoHandler implements HttpHandler {
+  private final ObjectMapper objectMapper;
+  private final DB database;
+
+  UpdatesMongoHandler(ObjectMapper objectMapper, DB database) {
+    this.objectMapper = Objects.requireNonNull(objectMapper);
+    this.database = Objects.requireNonNull(database);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    if (exchange.isInIoThread()) {
+      exchange.dispatch(this);
+      return;
+    }
+    int queries = Helper.getQueries(exchange);
+    World[] worlds = new World[queries];
+    for (int i = 0; i < queries; i++) {
+      int id = Helper.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);
+      
+      @SuppressWarnings("unused")
+      // Per test requirement the old value must be read
+      int oldRandomNumber = ((Number) object.get("randomNumber")).intValue(); 
+      
+      int newRandomNumber = Helper.randomWorld();
+      object.put("randomNumber", newRandomNumber);
+      database.getCollection("World").update(key, object);
+      worlds[i] = new World(id, newRandomNumber);
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, JSON_UTF8);
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 63 - 0
undertow-edge/src/main/java/hello/UpdatesSqlHandler.java

@@ -0,0 +1,63 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.Objects;
+
+import static hello.HelloWebServer.JSON_UTF8;
+
+/**
+ * Handles the updates test using a SQL database.
+ */
+final class UpdatesSqlHandler implements HttpHandler {
+  private final ObjectMapper objectMapper;
+  private final DataSource database;
+
+  UpdatesSqlHandler(ObjectMapper objectMapper, DataSource database) {
+    this.objectMapper = Objects.requireNonNull(objectMapper);
+    this.database = Objects.requireNonNull(database);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    if (exchange.isInIoThread()) {
+      exchange.dispatch(this);
+      return;
+    }
+    int queries = Helper.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, Helper.randomWorld());
+        World world;
+        try (ResultSet resultSet = query.executeQuery()) {
+          resultSet.next();
+          world = new World(
+              resultSet.getInt("id"),
+              resultSet.getInt("randomNumber"));
+        }
+        world.randomNumber = Helper.randomWorld();
+        update.setInt(1, world.randomNumber);
+        update.setInt(2, world.id);
+        update.executeUpdate();
+        worlds[i] = world;
+      }
+    }
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, JSON_UTF8);
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 20 - 0
undertow-edge/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-edge/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-edge/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