Browse Source

Undertow: lots of code movement but only a couple of functional changes

The functional changes were:
1) Offload blocking requests to worker threads using exchange.dispatch
2) Use default numbers for IO/worker threads

The rest of this commit is just shifting code around.  Individual handlers
went into their own classes.
Michael Hixson 12 years ago
parent
commit
4efd633227

+ 36 - 0
undertow/src/main/java/hello/CacheHandler.java

@@ -0,0 +1,36 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.cache.LoadingCache;
+import com.google.common.net.MediaType;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import java.util.Objects;
+
+/**
+ * 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, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 49 - 0
undertow/src/main/java/hello/DbMongoHandler.java

@@ -0,0 +1,49 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.net.MediaType;
+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;
+
+/**
+ * Handles the single- and multiple-query database tests using MongoDB.
+ */
+final class DbMongoHandler implements HttpHandler {
+  private final ObjectMapper objectMapper;
+  private final DB database;
+
+  DbMongoHandler(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++) {
+      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, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 54 - 0
undertow/src/main/java/hello/DbSqlHandler.java

@@ -0,0 +1,54 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.net.MediaType;
+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;
+
+/**
+ * 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;
+
+  DbSqlHandler(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 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, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 54 - 0
undertow/src/main/java/hello/FortunesMongoHandler.java

@@ -0,0 +1,54 @@
+package hello;
+
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import com.google.common.net.MediaType;
+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;
+
+/**
+ * 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, MediaType.HTML_UTF_8.toString());
+    exchange.getResponseSender().send(writer.toString());
+  }
+}

+ 60 - 0
undertow/src/main/java/hello/FortunesSqlHandler.java

@@ -0,0 +1,60 @@
+package hello;
+
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import com.google.common.net.MediaType;
+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;
+
+/**
+ * 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, MediaType.HTML_UTF_8.toString());
+    exchange.getResponseSender().send(writer.toString());
+  }
+}

+ 33 - 433
undertow/src/main/java/hello/HelloWebServer.java

@@ -2,43 +2,24 @@ package hello;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 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.server.HttpHandler;
-import io.undertow.server.HttpServerExchange;
 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
@@ -57,78 +38,6 @@ public final class HelloWebServer {
     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);
-  }
-
-  // Fields and constructor
-
-  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;
-
   /**
    * Creates and starts a new web server whose configuration is specified in the
    * {@code server.properties} file.
@@ -140,25 +49,28 @@ public final class HelloWebServer {
    */
   public HelloWebServer() throws IOException, SQLException {
     Properties properties = new Properties();
-    try (InputStream in = getClass().getResourceAsStream("server.properties")) {
+    try (InputStream in = HelloWebServer.class.getResourceAsStream(
+        "server.properties")) {
       properties.load(in);
     }
-    mysql = newDataSource(
+    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"));
-    postgresql = newDataSource(
+    final DataSource postgresql = Helper.newDataSource(
         properties.getProperty("postgresql.uri"),
         properties.getProperty("postgresql.user"),
         properties.getProperty("postgresql.password"));
-    mongodb = new MongoClient(properties.getProperty("mongodb.host"))
+    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.
     //
-    worldCache = CacheBuilder.newBuilder()
+    final LoadingCache<Integer, World> worldCache = CacheBuilder.newBuilder()
         .build(new CacheLoader<Integer, World>() {
           @Override
           public World load(Integer id) throws Exception {
@@ -195,345 +107,33 @@ public final class HelloWebServer {
             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")))
+            .addPath("/json",
+                new JsonHandler(objectMapper))
+            .addPath("/db/mysql",
+                new DbSqlHandler(objectMapper, mysql))
+            .addPath("/db/postgresql",
+                new DbSqlHandler(objectMapper, postgresql))
+            .addPath("/db/mongodb",
+                new DbMongoHandler(objectMapper, mongodb))
+            .addPath("/fortunes/mysql",
+                new FortunesSqlHandler(mustacheFactory, mysql))
+            .addPath("/fortunes/postgresql",
+                new FortunesSqlHandler(mustacheFactory, postgresql))
+            .addPath("/fortunes/mongodb",
+                new FortunesMongoHandler(mustacheFactory, mongodb))
+            .addPath("/updates/mysql",
+                new UpdatesSqlHandler(objectMapper, mysql))
+            .addPath("/updates/postgresql",
+                new UpdatesSqlHandler(objectMapper, postgresql))
+            .addPath("/updates/mongodb",
+                new UpdatesMongoHandler(objectMapper, mongodb))
+            .addPath("/plaintext",
+                new PlaintextHandler())
+            .addPath("/cache",
+                new CacheHandler(objectMapper, worldCache)),
+            Headers.SERVER_STRING, "undertow")))
         .build()
         .start();
   }
-
-  // Request handlers
-
-  /**
-   * 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));
-  }
-
-  /**
-   * 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());
-  }
-
-  /**
-   * 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());
-  }
-
-  /**
-   * 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"));
-        }
-        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));
-  }
 }

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

+ 30 - 0
undertow/src/main/java/hello/JsonHandler.java

@@ -0,0 +1,30 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.net.MediaType;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * 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, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(
+        objectMapper.writeValueAsString(
+            Collections.singletonMap("message", "Hello, World!")));
+  }
+}

+ 18 - 0
undertow/src/main/java/hello/PlaintextHandler.java

@@ -0,0 +1,18 @@
+package hello;
+
+import com.google.common.net.MediaType;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+
+/**
+ * Handles the plaintext test.
+ */
+final class PlaintextHandler implements HttpHandler {
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    exchange.getResponseHeaders().put(
+        Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString());
+    exchange.getResponseSender().send("Hello, World!");
+  }
+}

+ 54 - 0
undertow/src/main/java/hello/UpdatesMongoHandler.java

@@ -0,0 +1,54 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.net.MediaType;
+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;
+
+/**
+ * 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);
+      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, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}

+ 62 - 0
undertow/src/main/java/hello/UpdatesSqlHandler.java

@@ -0,0 +1,62 @@
+package hello;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.net.MediaType;
+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;
+
+/**
+ * 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, MediaType.JSON_UTF_8.toString());
+    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+  }
+}