Эх сурвалжийг харах

Update the Undertow tests (after round 14) (#2733)

* Update the Undertow tests

- Upgrade dependencies.

- The upgrade of the MongoDB driver from 2 to 3 came with a fair amount
  of code changes.

- Add tests for the MongoDB async driver.

- Remove the cache access test implementation.  We never got around to
  officially adding that as a test type.

- Change the SQL connection pool implementation from commons dbcp to
  HikariCP, which seems to be held in higher regard these days.

- Remove unnecessary tweaking of some Undertow.Builder settings that are
  now the default settings, e.g. including the "Date" HTTP header.

- Remove unproven tweaking of IO thread pool and worker thread pool
  sizes.  I believe those pool sizes were chosen by me, ages ago, for a
  much different version of Undertow on much different hardware.

- Split out single- and multi-query database test implementations into
  separate classes.  The two test types are different enough problems to
  deserve different solutions.

- Remove server-side concurrency fan-out for SQL multi-query and updates
  tests.  I would expect that to help with latency under low load, but
  I would expect it to harm max throughput.  Undertow's performance in
  these tests in recent rounds has been far below that of frameworks that
  should have similar numbers (e.g. servlet), and I suspect this fan-out
  is the cause.

- Avoid creating database connection pools that aren't being tested.
  For example, don't create a connection pool for the PostgreSQL database
  when MySQL is being tested, and don't create any connection pools when
  only JSON and plaintext are being tested.

- Fix the README.

- Simplify JDBC settings.

- The choice of 32 for database connection pool sizes is somewhat
  arbitrary.  I will not be surprised if there is a better size.  It
  seems tricky to size the pools correctly for all environments.

- I don't necessarily expect this to improve Undertow's numbers in all
  test types and configurations.  In local testing, the plaintext numbers
  are 20% worse (narrowed this one down a bit... the drop is caused by
  upgrading undertow-code from 1.2.x to 1.3.x or 1.4.x).  The MongoDB
  numbers (non-async) worsened by a similar amount (I blame the upgrade
  of the driver from 2.x to 3.x).

* Undertow: avoid batch updates in SQL

They caused deadlocks for PostgreSQL given high enough concurrency and
connection pool sizes, and I don't think they do anything for MySQL.

* Undertow: allow enough blocking connections to MongoDB

Without this, a large enough Undertow worker thread pool and a small
enough MongoDB connection pool will cause many requests to fail.

* Undertow: try switching host to ipv4-only to satisfy Travis
Michael Hixson 8 жил өмнө
parent
commit
d53ea3f0d3
31 өөрчлөгдсөн 1141 нэмэгдсэн , 737 устгасан
  1. 105 9
      frameworks/Java/undertow/README.md
  2. 38 17
      frameworks/Java/undertow/benchmark_config.json
  3. 115 113
      frameworks/Java/undertow/pom.xml
  4. 3 5
      frameworks/Java/undertow/setup.sh
  5. 3 1
      frameworks/Java/undertow/setup_mongodb.sh
  6. 7 0
      frameworks/Java/undertow/setup_mongodb_async.sh
  7. 3 1
      frameworks/Java/undertow/setup_mysql.sh
  8. 5 0
      frameworks/Java/undertow/setup_no_database.sh
  9. 3 1
      frameworks/Java/undertow/setup_postgresql.sh
  10. 34 0
      frameworks/Java/undertow/src/main/java/hello/AsyncHandler.java
  11. 0 37
      frameworks/Java/undertow/src/main/java/hello/CacheHandler.java
  12. 39 0
      frameworks/Java/undertow/src/main/java/hello/DbMongoAsyncHandler.java
  13. 18 54
      frameworks/Java/undertow/src/main/java/hello/DbMongoHandler.java
  14. 18 67
      frameworks/Java/undertow/src/main/java/hello/DbSqlHandler.java
  15. 5 9
      frameworks/Java/undertow/src/main/java/hello/Fortune.java
  16. 40 0
      frameworks/Java/undertow/src/main/java/hello/FortunesMongoAsyncHandler.java
  17. 16 37
      frameworks/Java/undertow/src/main/java/hello/FortunesMongoHandler.java
  18. 14 33
      frameworks/Java/undertow/src/main/java/hello/FortunesSqlHandler.java
  19. 205 144
      frameworks/Java/undertow/src/main/java/hello/HelloWebServer.java
  20. 130 48
      frameworks/Java/undertow/src/main/java/hello/Helper.java
  21. 7 19
      frameworks/Java/undertow/src/main/java/hello/JsonHandler.java
  22. 18 19
      frameworks/Java/undertow/src/main/java/hello/PlaintextHandler.java
  23. 58 0
      frameworks/Java/undertow/src/main/java/hello/QueriesMongoAsyncHandler.java
  24. 37 0
      frameworks/Java/undertow/src/main/java/hello/QueriesMongoHandler.java
  25. 44 0
      frameworks/Java/undertow/src/main/java/hello/QueriesSqlHandler.java
  26. 85 0
      frameworks/Java/undertow/src/main/java/hello/UpdatesMongoAsyncHandler.java
  27. 33 42
      frameworks/Java/undertow/src/main/java/hello/UpdatesMongoHandler.java
  28. 31 52
      frameworks/Java/undertow/src/main/java/hello/UpdatesSqlHandler.java
  29. 0 6
      frameworks/Java/undertow/src/main/java/hello/World.java
  30. 11 11
      frameworks/Java/undertow/src/main/resources/hello/fortunes.mustache
  31. 16 12
      frameworks/Java/undertow/src/main/resources/hello/server.properties

+ 105 - 9
frameworks/Java/undertow/README.md

@@ -1,15 +1,111 @@
-# Undertow Benchmarking Test
+# Undertow
 
-This is the undertow portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+This is the test for the Undertow web server.
 
-### JSON Encoding Test
-* [JSON test source](src/main/java/hello/HelloWebServer.java)
+* [Project website](http://undertow.io/)
+* [GitHub repository](https://github.com/undertow-io/undertow)
 
-## Versions
-Undertow 1.0.0.Alpha19 (http://undertow.io)
+## Test types
 
-## Test URLs
+This implements all benchmark test types.  The database tests are implemented
+for MySQL, PostgreSQL, and MongoDB databases.
 
-### JSON Encoding Test
+[HelloWebServer.java](src/main/java/hello/HelloWebServer.java) is the entry
+point for the application, providing the `main` method.
 
-    http://localhost:8080
+**The only test type that exercises Undertow in isolation is the plaintext
+test**.  For functionality that Undertow does not provide — JSON encoding,
+database connectivity — this implementation depends on popular third party
+libraries that are expected to perform well.  We hope for these tests to serve
+as performance baselines for benchmarks of other frameworks that are implemented
+on top of Undertow.
+
+### Plaintext
+
+URL: `http://TFB-server:8080/plaintext`
+
+Source code:
+* [PlaintextHandler.java](src/main/java/hello/PlaintextHandler.java)
+
+Additional libraries used: (None)
+
+### JSON
+
+URL: `http://TFB-server:8080/json`
+
+Source code:
+* [JsonHandler.java](src/main/java/hello/JsonHandler.java)
+
+Additional libraries used:
+* [Jackson]
+
+### Database single-query
+
+URL: `http://TFB-server:8080/db`
+
+Source code:
+* [DbSqlHandler.java](src/main/java/hello/DbSqlHandler.java)
+* [DbMongoHandler.java](src/main/java/hello/DbMongoHandler.java)
+* [DbMongoAsyncHandler.java](src/main/java/hello/DbMongoAsyncHandler.java)
+
+Additional libraries used:
+* [Jackson]
+* [MySQL Connector/J]
+* [PostgreSQL JDBC Driver]
+* [Java MongoDB Driver]
+* [HikariCP]
+
+### Database multi-query
+
+URL: `http://TFB-server:8080/queries?queries={integer}`
+
+Source code:
+* [QueriesSqlHandler.java](src/main/java/hello/QueriesSqlHandler.java)
+* [QueriesMongoHandler.java](src/main/java/hello/QueriesMongoHandler.java)
+* [QueriesMongoAsyncHandler.java](src/main/java/hello/QueriesMongoAsyncHandler.java)
+
+Additional libraries used:
+* [Jackson]
+* [MySQL Connector/J]
+* [PostgreSQL JDBC Driver]
+* [Java MongoDB Driver]
+* [HikariCP]
+
+### Database updates
+
+URL: `http://TFB-server:8080/updates?queries={integer}`
+
+Source code:
+* [UpdatesSqlHandler.java](src/main/java/hello/UpdatesSqlHandler.java)
+* [UpdatesMongoHandler.java](src/main/java/hello/UpdatesMongoHandler.java)
+* [UpdatesMongoAsyncHandler.java](src/main/java/hello/UpdatesMongoAsyncHandler.java)
+
+Additional libraries used:
+* [Jackson]
+* [MySQL Connector/J]
+* [PostgreSQL JDBC Driver]
+* [Java MongoDB Driver]
+* [HikariCP]
+
+### Fortunes
+
+URL: `http://TFB-server:8080/fortunes`
+
+Source code:
+* [FortunesSqlHandler.java](src/main/java/hello/FortunesSqlHandler.java)
+* [FortunesMongoHandler.java](src/main/java/hello/FortunesMongoHandler.java)
+* [FortunesMongoAsyncHandler.java](src/main/java/hello/FortunesMongoAsyncHandler.java)
+
+Additional libraries used:
+* [Mustache.java]
+* [MySQL Connector/J]
+* [PostgreSQL JDBC Driver]
+* [Java MongoDB Driver]
+* [HikariCP]
+
+[Jackson]: https://github.com/FasterXML/Jackson
+[Mustache.java]: https://github.com/spullara/mustache.java
+[MySQL Connector/J]: https://dev.mysql.com/downloads/connector/j/5.1.html
+[PostgreSQL JDBC Driver]: https://jdbc.postgresql.org/
+[Java MongoDB Driver]: https://docs.mongodb.com/ecosystem/drivers/java/
+[HikariCP]: https://github.com/brettwooldridge/HikariCP

+ 38 - 17
frameworks/Java/undertow/benchmark_config.json

@@ -2,10 +2,9 @@
   "framework": "undertow",
   "tests": [{
     "default": {
-      "setup_file": "setup",
+      "setup_file": "setup_no_database",
       "json_url": "/json",
       "plaintext_url": "/plaintext",
-      "cache_url": "/cache",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Platform",
@@ -24,10 +23,10 @@
     },
     "mysql" : {
       "setup_file": "setup_mysql",
-      "db_url": "/db/mysql",
-      "query_url": "/queries/mysql?queries=",
-      "fortune_url": "/fortunes/mysql",
-      "update_url": "/updates/mysql?queries=",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Platform",
@@ -40,16 +39,16 @@
       "webserver": "None",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "undertow",
+      "display_name": "undertow-mysql",
       "notes": "",
       "versus": ""
     },
     "postgresql" : {
       "setup_file": "setup_postgresql",
-      "db_url": "/db/postgresql",
-      "query_url": "/queries/postgresql?queries=",
-      "fortune_url": "/fortunes/postgresql",
-      "update_url": "/updates/postgresql?queries=",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Platform",
@@ -62,16 +61,16 @@
       "webserver": "None",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "undertow",
+      "display_name": "undertow-postgresql",
       "notes": "",
       "versus": ""
     },
     "mongodb" : {
       "setup_file": "setup_mongodb",
-      "db_url": "/db/mongodb",
-      "query_url": "/queries/mongodb?queries=",
-      "fortune_url": "/fortunes/mongodb",
-      "update_url": "/updates/mongodb?queries=",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Platform",
@@ -84,7 +83,29 @@
       "webserver": "None",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "undertow",
+      "display_name": "undertow-mongodb",
+      "notes": "",
+      "versus": ""
+    },
+    "mongodb-async" : {
+      "setup_file": "setup_mongodb_async",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "MongoDB",
+      "framework": "None",
+      "language": "Java",
+      "flavor": "None",
+      "orm": "Raw",
+      "platform": "Undertow",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "undertow-mongodb-async",
       "notes": "",
       "versus": ""
     }

+ 115 - 113
frameworks/Java/undertow/pom.xml

@@ -2,120 +2,122 @@
 <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>
 
-    <groupId>com.techempower</groupId>
-    <artifactId>undertow-example</artifactId>
-    <version>0.1</version>
-     <properties>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    </properties>
-    <dependencies>
-        <!-- Web server -->
-        <dependency>
-            <groupId>io.undertow</groupId>
-            <artifactId>undertow-core</artifactId>
-            <version>1.2.5.Final</version>
-        </dependency>
-        <dependency>
-            <groupId>org.jboss.xnio</groupId>
-            <artifactId>xnio-api</artifactId>
-            <version>3.3.1.Final</version>
-        </dependency>
-        <dependency>
-            <groupId>org.jboss.xnio</groupId>
-            <artifactId>xnio-nio</artifactId>
-            <version>3.3.1.Final</version>
-        </dependency>
-        <!-- Database drivers -->
-        <dependency>
-            <groupId>mysql</groupId>
-            <artifactId>mysql-connector-java</artifactId>
-            <version>5.1.38</version>
-        </dependency>
-        <dependency>
-            <groupId>org.postgresql</groupId>
-	    <artifactId>postgresql</artifactId>
-            <version>9.4.1208</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>18.0</version>
-        </dependency>
-        <!-- JSON encoding -->
-        <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-databind</artifactId>
-            <version>2.5.3</version>
-        </dependency>
-        <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-annotations</artifactId>
-            <version>2.5.3</version>
-        </dependency>
-        <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-core</artifactId>
-            <version>2.5.3</version>
-        </dependency>
-        <!-- HTML templates -->
-        <dependency>
-            <groupId>com.github.spullara.mustache.java</groupId>
-            <artifactId>compiler</artifactId>
-            <version>0.9.0</version>
-        </dependency>
-    </dependencies>
+  <modelVersion>4.0.0</modelVersion>
 
-    <build>
-        <plugins>
-            <plugin>
-                <inherited>true</inherited>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.1</version>
-                <configuration>
-                    <source>1.8</source>
-                    <target>1.8</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>
+  <groupId>hello</groupId>
+  <artifactId>hello-undertow</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <properties>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <version.hikaricp>2.6.1</version.hikaricp>
+    <version.jackson>2.8.8.1</version.jackson>
+    <version.maven-shade-plugin>3.0.0</version.maven-shade-plugin>
+    <version.mongodb>3.4.2</version.mongodb>
+    <version.mustache>0.9.4</version.mustache>
+    <version.mysql>5.1.41</version.mysql>
+    <version.postgresql>9.4.1212</version.postgresql>
+    <version.undertow>1.4.13.Final</version.undertow>
+  </properties>
+
+  <prerequisites>
+    <maven>3.0</maven>
+  </prerequisites>
+
+  <dependencies>
+    <dependency>
+      <groupId>io.undertow</groupId>
+      <artifactId>undertow-core</artifactId>
+      <version>${version.undertow}</version>
+    </dependency>
+    <dependency>
+      <groupId>mysql</groupId>
+      <artifactId>mysql-connector-java</artifactId>
+      <version>${version.mysql}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.postgresql</groupId>
+      <artifactId>postgresql</artifactId>
+      <version>${version.postgresql}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mongodb</groupId>
+      <artifactId>mongodb-driver</artifactId>
+      <version>${version.mongodb}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mongodb</groupId>
+      <artifactId>mongodb-driver-core</artifactId>
+      <version>${version.mongodb}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mongodb</groupId>
+      <artifactId>mongodb-driver-async</artifactId>
+      <version>${version.mongodb}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mongodb</groupId>
+      <artifactId>bson</artifactId>
+      <version>${version.mongodb}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.zaxxer</groupId>
+      <artifactId>HikariCP</artifactId>
+      <version>${version.hikaricp}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>${version.jackson}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.github.spullara.mustache.java</groupId>
+      <artifactId>compiler</artifactId>
+      <version>${version.mustache}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <finalName>hello-undertow</finalName>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>${version.maven-shade-plugin}</version>
+        <configuration>
+          <createDependencyReducedPom>false</createDependencyReducedPom>
+          <filters>
+            <filter>
+              <artifact>*:*</artifact>
+              <excludes>
+                <exclude>META-INF/*.SF</exclude>
+                <exclude>META-INF/*.DSA</exclude>
+                <exclude>META-INF/*.RSA</exclude>
+              </excludes>
+            </filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <transformers>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                  <mainClass>hello.HelloWebServer</mainClass>
+                </transformer>
+              </transformers>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 
 </project>

+ 3 - 5
frameworks/Java/undertow/setup.sh

@@ -1,9 +1,7 @@
 #!/bin/bash
 
-sed -i 's|DATABASE_HOST|'"${DBHOST}"'|g' src/main/resources/hello/server.properties
+fw_depends java maven
 
-fw_depends mongodb postgresql mysql java maven
+mvn clean package
 
-mvn clean compile assembly:single
-cd target
-java -jar undertow-example-0.1-jar-with-dependencies.jar &
+java -jar target/hello-undertow.jar $UNDERTOW_ARGS

+ 3 - 1
frameworks/Java/undertow/setup_mongodb.sh

@@ -2,4 +2,6 @@
 
 fw_depends mongodb
 
-source ./setup.sh
+export UNDERTOW_ARGS="MONGODB"
+
+source ./setup.sh

+ 7 - 0
frameworks/Java/undertow/setup_mongodb_async.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+
+fw_depends mongodb
+
+export UNDERTOW_ARGS="MONGODB_ASYNC"
+
+source ./setup.sh

+ 3 - 1
frameworks/Java/undertow/setup_mysql.sh

@@ -2,4 +2,6 @@
 
 fw_depends mysql
 
-source ./setup.sh
+export UNDERTOW_ARGS="MYSQL"
+
+source ./setup.sh

+ 5 - 0
frameworks/Java/undertow/setup_no_database.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+export UNDERTOW_ARGS="NO_DATABASE"
+
+source ./setup.sh

+ 3 - 1
frameworks/Java/undertow/setup_postgresql.sh

@@ -2,4 +2,6 @@
 
 fw_depends postgresql
 
-source ./setup.sh
+export UNDERTOW_ARGS="POSTGRESQL"
+
+source ./setup.sh

+ 34 - 0
frameworks/Java/undertow/src/main/java/hello/AsyncHandler.java

@@ -0,0 +1,34 @@
+package hello;
+
+import static hello.Helper.sendException;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.SameThreadExecutor;
+import java.util.Objects;
+
+/**
+ * An HTTP handler that <em>does not</em> end the exchange when the call stack
+ * of its {@link HttpHandler#handleRequest(HttpServerExchange)} method returns.
+ * The handler must ensure that every exchange is ended through other means.
+ */
+final class AsyncHandler implements HttpHandler {
+  private final HttpHandler handler;
+
+  AsyncHandler(HttpHandler handler) {
+    this.handler = Objects.requireNonNull(handler);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) {
+    Runnable asyncTask =
+        () -> {
+          try {
+            handler.handleRequest(exchange);
+          } catch (Exception e) {
+            sendException(exchange, e);
+          }
+        };
+    exchange.dispatch(SameThreadExecutor.INSTANCE, asyncTask);
+  }
+}

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

@@ -1,37 +0,0 @@
-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));
-  }
-}

+ 39 - 0
frameworks/Java/undertow/src/main/java/hello/DbMongoAsyncHandler.java

@@ -0,0 +1,39 @@
+package hello;
+
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendException;
+import static hello.Helper.sendJson;
+
+import com.mongodb.async.client.MongoCollection;
+import com.mongodb.async.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import org.bson.Document;
+
+/**
+ * Handles the single-query database test using MongoDB with an asynchronous
+ * API.
+ */
+final class DbMongoAsyncHandler implements HttpHandler {
+  private final MongoCollection<Document> worldCollection;
+
+  DbMongoAsyncHandler(MongoDatabase db) {
+    worldCollection = db.getCollection("world");
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) {
+    worldCollection
+        .find(Filters.eq(randomWorld()))
+        .map(Helper::mongoDocumentToWorld)
+        .first(
+            (world, exception) -> {
+              if (exception != null) {
+                sendException(exchange, exception);
+              } else {
+                sendJson(exchange, world);
+              }
+            });
+  }
+}

+ 18 - 54
frameworks/Java/undertow/src/main/java/hello/DbMongoHandler.java

@@ -1,68 +1,32 @@
 package hello;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.mongodb.BasicDBObject;
-import com.mongodb.DB;
-import com.mongodb.DBObject;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendJson;
+
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
 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;
+import org.bson.Document;
 
 /**
- * Handles the single- and multiple-query database tests using MongoDB.
+ * Handles the single-query database test using MongoDB.
  */
 final class DbMongoHandler implements HttpHandler {
-  private final ObjectMapper objectMapper;
-  private final DB database;
-  private final boolean multiple;
+  private final MongoCollection<Document> worldCollection;
 
-  DbMongoHandler(ObjectMapper objectMapper, DB database, boolean multiple) {
-    this.objectMapper = Objects.requireNonNull(objectMapper);
-    this.database = Objects.requireNonNull(database);
-    this.multiple = multiple;
+  DbMongoHandler(MongoDatabase db) {
+    worldCollection = db.getCollection("world");
   }
 
   @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]));
-    }
+  public void handleRequest(HttpServerExchange exchange) {
+    World world =
+        worldCollection
+            .find(Filters.eq(randomWorld()))
+            .map(Helper::mongoDocumentToWorld)
+            .first();
+    sendJson(exchange, world);
   }
 }

+ 18 - 67
frameworks/Java/undertow/src/main/java/hello/DbSqlHandler.java

@@ -1,89 +1,40 @@
 package hello;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendJson;
 
 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.Map;
 import java.util.Objects;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Future;
-
-import static hello.HelloWebServer.JSON_UTF8;
+import javax.sql.DataSource;
 
 /**
- * Handles the single- and multiple-query database tests using a SQL database.
+ * Handles the single-query database test using a SQL database.
  */
 final class DbSqlHandler implements HttpHandler {
-  private final ObjectMapper objectMapper;
-  private final DataSource database;
-  private final boolean multiple;
+  private final DataSource db;
 
-  DbSqlHandler(ObjectMapper objectMapper, DataSource database, boolean multiple) {
-    this.objectMapper = Objects.requireNonNull(objectMapper);
-    this.database = Objects.requireNonNull(database);
-    this.multiple = multiple;
+  DbSqlHandler(DataSource db) {
+    this.db = Objects.requireNonNull(db);
   }
 
   @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 (final Connection connection = database.getConnection()) {
-      Map<Integer, Future<World>> futureWorlds = new ConcurrentHashMap<>();
-      for (int i = 0; i < queries; i++) {
-        futureWorlds.put(i, Helper.EXECUTOR.submit(new Callable<World>(){
-          @Override
-          public World call() throws Exception {
-            try (PreparedStatement statement = connection.prepareStatement(
-                "SELECT * FROM World WHERE id = ?",
-                ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
-
-              statement.setInt(1, Helper.randomWorld());
-              ResultSet resultSet = statement.executeQuery();
-              resultSet.next();
-              return new World(
-                resultSet.getInt("id"),
-                resultSet.getInt("randomNumber"));
-            }
-          }
-        }));
-      }
-
-      for (int i = 0; i < queries; i++) {
-        worlds[i] = futureWorlds.get(i).get();
+    World world;
+    try (Connection connection = db.getConnection();
+         PreparedStatement statement =
+             connection.prepareStatement("SELECT * FROM World WHERE id = ?")) {
+      statement.setInt(1, randomWorld());
+      try (ResultSet resultSet = statement.executeQuery()) {
+        resultSet.next();
+        int id = resultSet.getInt("id");
+        int randomNumber = resultSet.getInt("randomNumber");
+        world = new World(id, 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]));
-    }
+    sendJson(exchange, world);
   }
 }

+ 5 - 9
frameworks/Java/undertow/src/main/java/hello/Fortune.java

@@ -1,21 +1,17 @@
 package hello;
 
+import java.util.Objects;
+
 /**
  * The model for the "fortune" database table.
  */
 public final class Fortune implements Comparable<Fortune> {
-  public int id;
-  public String message;
+  public final int id;
+  public final 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;
+    this.message = Objects.requireNonNull(message);
   }
 
   @Override

+ 40 - 0
frameworks/Java/undertow/src/main/java/hello/FortunesMongoAsyncHandler.java

@@ -0,0 +1,40 @@
+package hello;
+
+import static hello.Helper.sendException;
+import static hello.Helper.sendHtml;
+
+import com.mongodb.async.client.MongoCollection;
+import com.mongodb.async.client.MongoDatabase;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import java.util.ArrayList;
+import org.bson.Document;
+
+/**
+ * Handles the fortunes test using MongoDB with an asynchronous API.
+ */
+final class FortunesMongoAsyncHandler implements HttpHandler {
+  private final MongoCollection<Document> fortuneCollection;
+
+  FortunesMongoAsyncHandler(MongoDatabase db) {
+    fortuneCollection = db.getCollection("fortune");
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) {
+    fortuneCollection
+        .find()
+        .map(Helper::mongoDocumentToFortune)
+        .into(
+            new ArrayList<>(),
+            (fortunes, exception) -> {
+              if (exception != null) {
+                sendException(exchange, exception);
+              } else {
+                fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+                fortunes.sort(null);
+                sendHtml(exchange, fortunes, "hello/fortunes.mustache");
+              }
+            });
+  }
+}

+ 16 - 37
frameworks/Java/undertow/src/main/java/hello/FortunesMongoHandler.java

@@ -1,55 +1,34 @@
 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 static hello.Helper.sendHtml;
+
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
 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;
+import org.bson.Document;
 
 /**
  * Handles the fortunes test using MongoDB.
  */
 final class FortunesMongoHandler implements HttpHandler {
-  private final MustacheFactory mustacheFactory;
-  private final DB database;
+  private final MongoCollection<Document> fortuneCollection;
 
-  FortunesMongoHandler(MustacheFactory mustacheFactory, DB database) {
-    this.mustacheFactory = Objects.requireNonNull(mustacheFactory);
-    this.database = Objects.requireNonNull(database);
+  FortunesMongoHandler(MongoDatabase db) {
+    fortuneCollection = db.getCollection("fortune");
   }
 
   @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")));
-    }
+  public void handleRequest(HttpServerExchange exchange) {
+    List<Fortune> fortunes =
+        fortuneCollection
+            .find()
+            .map(Helper::mongoDocumentToFortune)
+            .into(new ArrayList<>());
     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());
+    fortunes.sort(null);
+    sendHtml(exchange, fortunes, "hello/fortunes.mustache");
   }
 }

+ 14 - 33
frameworks/Java/undertow/src/main/java/hello/FortunesSqlHandler.java

@@ -1,61 +1,42 @@
 package hello;
 
-import com.github.mustachejava.Mustache;
-import com.github.mustachejava.MustacheFactory;
+import static hello.Helper.sendHtml;
+
 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;
+import javax.sql.DataSource;
 
 /**
  * Handles the fortunes test using a SQL database.
  */
 final class FortunesSqlHandler implements HttpHandler {
-  private final MustacheFactory mustacheFactory;
-  private final DataSource database;
+  private final DataSource db;
 
-  FortunesSqlHandler(MustacheFactory mustacheFactory, DataSource database) {
-    this.mustacheFactory = Objects.requireNonNull(mustacheFactory);
-    this.database = Objects.requireNonNull(database);
+  FortunesSqlHandler(DataSource db) {
+    this.db = Objects.requireNonNull(db);
   }
 
   @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);
+    try (Connection connection = db.getConnection();
+         PreparedStatement statement =
+             connection.prepareStatement("SELECT * FROM Fortune");
          ResultSet resultSet = statement.executeQuery()) {
       while (resultSet.next()) {
-        fortunes.add(new Fortune(
-            resultSet.getInt("id"),
-            resultSet.getString("message")));
+        int id = resultSet.getInt("id");
+        String message = resultSet.getString("message");
+        fortunes.add(new Fortune(id, 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());
+    fortunes.sort(null);
+    sendHtml(exchange, fortunes, "hello/fortunes.mustache");
   }
 }

+ 205 - 144
frameworks/Java/undertow/src/main/java/hello/HelloWebServer.java

@@ -1,164 +1,225 @@
 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 com.mongodb.MongoClientOptions;
+import com.mongodb.ServerAddress;
+import com.mongodb.async.client.MongoClientSettings;
+import com.mongodb.async.client.MongoClients;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.connection.ClusterConnectionMode;
+import com.mongodb.connection.ClusterSettings;
+import com.mongodb.connection.ConnectionPoolSettings;
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
 import io.undertow.Undertow;
 import io.undertow.UndertowOptions;
-import io.undertow.util.Headers;
-import org.xnio.Options;
-
-import javax.sql.DataSource;
-import java.io.IOException;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.handlers.BlockingHandler;
+import io.undertow.server.handlers.PathHandler;
+import io.undertow.server.handlers.SetHeaderHandler;
 import java.io.InputStream;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
+import java.util.Collections;
 import java.util.Properties;
+import javax.sql.DataSource;
 
 /**
- * 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.
+ * Provides the {@link #main(String[])} method, which launches the application.
  */
 public final class HelloWebServer {
+  private HelloWebServer() {
+    throw new AssertionError();
+  }
 
-  //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 void main(String[] args) throws Exception {
+    Mode mode = Mode.valueOf(args[0]);
+    Properties props = new Properties();
+    try (InputStream in =
+             Thread.currentThread()
+                   .getContextClassLoader()
+                   .getResourceAsStream("hello/server.properties")) {
+      props.load(in);
+    }
+    int port = Integer.parseInt(props.getProperty("undertow.port"));
+    String host = props.getProperty("undertow.host");
+    HttpHandler paths = mode.paths(props);
+    HttpHandler rootHandler = new SetHeaderHandler(paths, "Server", "U-tow");
+    Undertow.builder()
+            .addHttpListener(port, host)
+            // In HTTP/1.1, connections are persistent unless declared
+            // otherwise.  Adding a "Connection: keep-alive" header to every
+            // response would only add useless bytes.
+            .setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false)
+            .setHandler(rootHandler)
+            .build()
+            .start();
+  }
 
-  public static final String TEXT_PLAIN = MediaType.PLAIN_TEXT_UTF_8.toString();
+  enum Mode {
+    /**
+     * The server will only implement the test types that do not require a
+     * database.
+     */
+    NO_DATABASE() {
+      @Override
+      HttpHandler paths(Properties props) {
+        return new PathHandler()
+            .addExactPath("/plaintext", new PlaintextHandler())
+            .addExactPath("/json",      new JsonHandler());
+      }
+    },
 
-  public static final String HTML_UTF8 = MediaType.HTML_UTF_8.toString();
+    /**
+     * The server will use a MySQL database and will only implement the test
+     * types that require a database.
+     */
+    MYSQL() {
+      @Override
+      HttpHandler paths(Properties props) {
+        String jdbcUrl = props.getProperty("mysql.jdbcUrl");
+        String username = props.getProperty("mysql.username");
+        String password = props.getProperty("mysql.password");
+        int connections = Integer.parseInt(props.getProperty("mysql.connections"));
+        DataSource db = newSqlDataSource(jdbcUrl, username, password, connections);
+        return new PathHandler()
+            .addExactPath("/db",       new BlockingHandler(new DbSqlHandler(db)))
+            .addExactPath("/queries",  new BlockingHandler(new QueriesSqlHandler(db)))
+            .addExactPath("/fortunes", new BlockingHandler(new FortunesSqlHandler(db)))
+            .addExactPath("/updates",  new BlockingHandler(new UpdatesSqlHandler(db)));
+      }
+    },
 
-  public static void main(String[] args) throws Exception {
-    new HelloWebServer();
-  }
+    /**
+     * The server will use a PostgreSQL database and will only implement the
+     * test types that require a database.
+     */
+    POSTGRESQL() {
+      @Override
+      HttpHandler paths(Properties props) {
+        String jdbcUrl = props.getProperty("postgresql.jdbcUrl");
+        String username = props.getProperty("postgresql.username");
+        String password = props.getProperty("postgresql.password");
+        int connections = Integer.parseInt(props.getProperty("postgresql.connections"));
+        DataSource db = newSqlDataSource(jdbcUrl, username, password, connections);
+        return new PathHandler()
+            .addExactPath("/db",       new BlockingHandler(new DbSqlHandler(db)))
+            .addExactPath("/queries",  new BlockingHandler(new QueriesSqlHandler(db)))
+            .addExactPath("/fortunes", new BlockingHandler(new FortunesSqlHandler(db)))
+            .addExactPath("/updates",  new BlockingHandler(new UpdatesSqlHandler(db)));
+      }
+    },
 
-  /**
-   * 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);
+    /**
+     * The server will use a MongoDB database and will only implement the test
+     * types that require a database.
+     */
+    MONGODB() {
+      @Override
+      HttpHandler paths(Properties props) {
+        String host = props.getProperty("mongodb.host");
+        String databaseName = props.getProperty("mongodb.databaseName");
+        int connections = Integer.parseInt(props.getProperty("mongodb.connections"));
+        MongoDatabase db = newMongoDatabase(host, databaseName, connections);
+        return new PathHandler()
+            .addExactPath("/db",       new BlockingHandler(new DbMongoHandler(db)))
+            .addExactPath("/queries",  new BlockingHandler(new QueriesMongoHandler(db)))
+            .addExactPath("/fortunes", new BlockingHandler(new FortunesMongoHandler(db)))
+            .addExactPath("/updates",  new BlockingHandler(new UpdatesMongoHandler(db)));
+      }
+    },
+
+    /**
+     * The server will use a MongoDB database with an asynchronous API and will
+     * only implement the test types that require a database.
+     */
+    MONGODB_ASYNC() {
+      @Override
+      HttpHandler paths(Properties props) {
+        String host = props.getProperty("mongodb.host");
+        String databaseName = props.getProperty("mongodb.databaseName");
+        int connections = Integer.parseInt(props.getProperty("mongodb.connections"));
+        com.mongodb.async.client.MongoDatabase db =
+            newMongoDatabaseAsync(host, databaseName, connections);
+        return new PathHandler()
+            .addExactPath("/db",       new AsyncHandler(new DbMongoAsyncHandler(db)))
+            .addExactPath("/queries",  new AsyncHandler(new QueriesMongoAsyncHandler(db)))
+            .addExactPath("/fortunes", new AsyncHandler(new FortunesMongoAsyncHandler(db)))
+            .addExactPath("/updates",  new AsyncHandler(new UpdatesMongoAsyncHandler(db)));
       }
+    };
+
+    /**
+     * Returns an HTTP handler that provides routing for all the
+     * test-type-specific endpoints of the server.
+     *
+     * @param props the server configuration
+     */
+    abstract HttpHandler paths(Properties props);
+
+    /**
+     * Provides a source of connections to a SQL database.
+     */
+    static DataSource newSqlDataSource(String jdbcUrl,
+                                       String username,
+                                       String password,
+                                       int connections) {
+      HikariConfig config = new HikariConfig();
+      config.setJdbcUrl(jdbcUrl);
+      config.setUsername(username);
+      config.setPassword(password);
+      config.setMinimumIdle(connections);
+      config.setMaximumPoolSize(connections);
+      return new HikariDataSource(config);
     }
-    Undertow.builder()
-        .addHttpListener(
-            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
-        .setSocketOption(Options.BACKLOG, 10000)
-        .setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false) //don't send a keep-alive header for HTTP/1.1 requests, as it is not required
-        .setServerOption(UndertowOptions.ALWAYS_SET_DATE, true)
-        .setServerOption(UndertowOptions.ENABLE_CONNECTOR_STATISTICS, false)
-        .setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, false)
-        .setHandler(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();
+
+    /**
+     * Provides a source of connections to a MongoDB database.
+     */
+    static MongoDatabase newMongoDatabase(String host,
+                                          String databaseName,
+                                          int connections) {
+      MongoClientOptions.Builder options = MongoClientOptions.builder();
+      options.minConnectionsPerHost(connections);
+      options.connectionsPerHost(connections);
+      options.threadsAllowedToBlockForConnectionMultiplier(
+          (int) Math.ceil((double) MAX_DB_REQUEST_CONCURRENCY / connections));
+      MongoClient client = new MongoClient(host, options.build());
+      return client.getDatabase(databaseName);
+    }
+
+    /**
+     * Provides a source of connections to a MongoDB database with an
+     * asynchronous API.
+     */
+    static com.mongodb.async.client.MongoDatabase
+    newMongoDatabaseAsync(String host,
+                          String databaseName,
+                          int connections) {
+      ClusterSettings clusterSettings =
+          ClusterSettings
+              .builder()
+              .mode(ClusterConnectionMode.SINGLE)
+              .hosts(Collections.singletonList(new ServerAddress(host)))
+              .build();
+      ConnectionPoolSettings connectionPoolSettings =
+          ConnectionPoolSettings
+              .builder()
+              .minSize(connections)
+              .maxSize(connections)
+              .maxWaitQueueSize(
+                  MAX_DB_REQUEST_CONCURRENCY * MAX_DB_QUERIES_PER_REQUEST)
+              .build();
+      MongoClientSettings clientSettings =
+          MongoClientSettings
+              .builder()
+              .clusterSettings(clusterSettings)
+              .connectionPoolSettings(connectionPoolSettings)
+              .build();
+      com.mongodb.async.client.MongoClient client =
+          MongoClients.create(clientSettings);
+      return client.getDatabase(databaseName);
+    }
+
+    private static final int MAX_DB_REQUEST_CONCURRENCY = 256;
+    private static final int MAX_DB_QUERIES_PER_REQUEST = 20;
   }
 }

+ 130 - 48
frameworks/Java/undertow/src/main/java/hello/Helper.java

@@ -1,55 +1,34 @@
 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 static io.undertow.util.Headers.CONTENT_TYPE;
 
-import javax.sql.DataSource;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import io.undertow.server.HttpServerExchange;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Deque;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import org.bson.Document;
 
 /**
- * Provides utility methods for the benchmark tests.
+ * Provides utility methods for the application.
  */
 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.
@@ -66,12 +45,13 @@ final class Helper {
     if (textValue == null) {
       return 1;
     }
+    int parsedValue;
     try {
-      int parsedValue = Integer.parseInt(textValue);
-      return Math.min(500, Math.max(1, parsedValue));
+      parsedValue = Integer.parseInt(textValue);
     } catch (NumberFormatException e) {
       return 1;
     }
+    return Math.min(500, Math.max(1, parsedValue));
   }
 
   /**
@@ -84,13 +64,115 @@ final class Helper {
     return 1 + ThreadLocalRandom.current().nextInt(10000);
   }
 
-  private static final int cpuCount = Runtime.getRuntime().availableProcessors();
+  /**
+   * Ends the HTTP exchange by encoding the given value as JSON and writing
+   * that JSON to the response.
+   *
+   * @param exchange the current HTTP exchange
+   * @param value the value to be encoded as JSON
+   * @throws IllegalArgumentException if the value cannot be encoded as JSON
+   */
+  static void sendJson(HttpServerExchange exchange, Object value) {
+    byte[] jsonBytes;
+    try {
+      jsonBytes = objectMapper.writeValueAsBytes(value);
+    } catch (IOException e) {
+      throw new IllegalArgumentException(e);
+    }
+    ByteBuffer jsonBuffer = ByteBuffer.wrap(jsonBytes);
+    exchange.getResponseHeaders().put(CONTENT_TYPE, "application/json");
+    exchange.getResponseSender().send(jsonBuffer);
+  }
+
+  private static final ObjectMapper objectMapper = new ObjectMapper();
+
+  /**
+   * Ends the HTTP exchange by supplying the given value to a Mustache template
+   * and writing the HTML output of the template to the response.
+   *
+   * @param exchange the current HTTP exchange
+   * @param value the value to be supplied to the Mustache template
+   * @param templatePath the path to the Mustache template
+   */
+  static void sendHtml(HttpServerExchange exchange,
+                       Object value,
+                       String templatePath) {
+    Mustache mustache = mustacheFactory.compile(templatePath);
+    StringWriter writer = new StringWriter();
+    mustache.execute(writer, value);
+    String html = writer.toString();
+    exchange.getResponseHeaders().put(CONTENT_TYPE, "text/html;charset=utf-8");
+    exchange.getResponseSender().send(html);
+  }
+
+  private static final MustacheFactory mustacheFactory =
+      new DefaultMustacheFactory();
 
-  // todo: parameterize multipliers
-  public static ExecutorService EXECUTOR =
-    new ThreadPoolExecutor(
-      cpuCount * 2, cpuCount * 25, 200, TimeUnit.MILLISECONDS,
-      new LinkedBlockingQueue<Runnable>(cpuCount * 100),
-      new ThreadPoolExecutor.CallerRunsPolicy());
+  /**
+   * Ends the HTTP exchange with an exception.
+   *
+   * @param exchange the current HTTP exchange
+   * @param exception the exception that was thrown
+   */
+  static void sendException(HttpServerExchange exchange, Throwable exception) {
+    exchange.setStatusCode(500);
+    exchange.endExchange();
+    exception.printStackTrace();
+  }
 
+  /**
+   * Reads a {@link World} from its persisted {@link Document} representation.
+   */
+  static World mongoDocumentToWorld(Document document) {
+    int id = mongoGetInt(document, "_id");
+    int randomNumber = mongoGetInt(document, "randomNumber");
+    return new World(id, randomNumber);
+  }
+
+  /**
+   * Reads a {@link Fortune} from its persisted {@link Document} representation.
+   */
+  static Fortune mongoDocumentToFortune(Document document) {
+    int id = mongoGetInt(document, "_id");
+    String message = document.getString("message");
+    return new Fortune(id, message);
+  }
+
+  // We don't know ahead of time whether these values are instances of Integer
+  // or Double.  This code is compatible with both.
+  private static int mongoGetInt(Document document, String key) {
+    return ((Number) document.get(key)).intValue();
+  }
+
+  /**
+   * Transforms a stream of futures ({@code Stream<CompletableFuture<T>>}) into
+   * a single future ({@code CompletableFuture<List<T>>}) containing all the
+   * values of the input futures.
+   */
+  static <T> Collector<
+      CompletableFuture<? extends T>,
+      ?,
+      CompletableFuture<List<T>>>
+  toCompletableFuture() {
+    return Collectors.collectingAndThen(
+        Collectors.toList(),
+        Helper::futureValuesOf);
+  }
+
+  private static <T> CompletableFuture<List<T>> futureValuesOf(
+      Collection<CompletableFuture<? extends T>> futures) {
+    CompletableFuture<?>[] futuresArray =
+        futures.toArray(new CompletableFuture<?>[0]);
+    CompletableFuture<Void> allComplete = CompletableFuture.allOf(futuresArray);
+    return allComplete.thenApply(
+        nil -> {
+          List<T> values = new ArrayList<>(futuresArray.length);
+          for (CompletableFuture<?> future : futuresArray) {
+            @SuppressWarnings("unchecked")
+            T value = (T) future.join();
+            values.add(value);
+          }
+          return Collections.unmodifiableList(values);
+        });
+  }
 }

+ 7 - 19
frameworks/Java/undertow/src/main/java/hello/JsonHandler.java

@@ -1,32 +1,20 @@
 package hello;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
+import static hello.Helper.sendJson;
+
 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;
+import java.util.Map;
 
 /**
  * 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!"))));
+  public void handleRequest(HttpServerExchange exchange) {
+    Map<String, String> value =
+        Collections.singletonMap("message", "Hello, World!");
+    sendJson(exchange, value);
   }
 }

+ 18 - 19
frameworks/Java/undertow/src/main/java/hello/PlaintextHandler.java

@@ -1,33 +1,32 @@
 package hello;
 
+import static io.undertow.util.Headers.CONTENT_TYPE;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
 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);
+  public void handleRequest(HttpServerExchange exchange) {
+    exchange.getResponseHeaders().put(CONTENT_TYPE, "text/plain");
     exchange.getResponseSender().send(buffer.duplicate());
   }
+
+  // Normally, one would send the string "Hello, World!" directly.  Reusing a
+  // ByteBuffer is a micro-optimization that is explicitly permitted by the
+  // plaintext test requirements.
+
+  private static final ByteBuffer buffer;
+  static {
+    String message = "Hello, World!";
+    byte[] messageBytes = message.getBytes(US_ASCII);
+    buffer = ByteBuffer.allocateDirect(messageBytes.length);
+    buffer.put(messageBytes);
+    buffer.flip();
+  }
 }

+ 58 - 0
frameworks/Java/undertow/src/main/java/hello/QueriesMongoAsyncHandler.java

@@ -0,0 +1,58 @@
+package hello;
+
+import static hello.Helper.getQueries;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendException;
+import static hello.Helper.sendJson;
+import static hello.Helper.toCompletableFuture;
+
+import com.mongodb.async.client.MongoCollection;
+import com.mongodb.async.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.IntStream;
+import org.bson.Document;
+
+/**
+ * Handles the multi-query database test using MongoDB with an asynchronous API.
+ */
+final class QueriesMongoAsyncHandler implements HttpHandler {
+  private final MongoCollection<Document> worldCollection;
+
+  QueriesMongoAsyncHandler(MongoDatabase db) {
+    worldCollection = db.getCollection("world");
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) {
+    IntStream
+        .range(0, getQueries(exchange))
+        .mapToObj(
+            i -> {
+              CompletableFuture<World> future = new CompletableFuture<>();
+              worldCollection
+                  .find(Filters.eq(randomWorld()))
+                  .map(Helper::mongoDocumentToWorld)
+                  .first(
+                      (world, exception) -> {
+                        if (exception != null) {
+                          future.completeExceptionally(exception);
+                        } else {
+                          future.complete(world);
+                        }
+                      });
+              return future;
+            })
+        .collect(toCompletableFuture())
+        .whenComplete(
+            (worlds, exception) -> {
+              if (exception != null) {
+                sendException(exchange, exception);
+              } else {
+                sendJson(exchange, worlds);
+              }
+            });
+  }
+}

+ 37 - 0
frameworks/Java/undertow/src/main/java/hello/QueriesMongoHandler.java

@@ -0,0 +1,37 @@
+package hello;
+
+import static hello.Helper.getQueries;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendJson;
+
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import org.bson.Document;
+
+/**
+ * Handles the multi-query database test using MongoDB.
+ */
+final class QueriesMongoHandler implements HttpHandler {
+  private final MongoCollection<Document> worldCollection;
+
+  QueriesMongoHandler(MongoDatabase db) {
+    worldCollection = db.getCollection("world");
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) {
+    int queries = getQueries(exchange);
+    World[] worlds = new World[queries];
+    for (int i = 0; i < worlds.length; i++) {
+      worlds[i] =
+          worldCollection
+              .find(Filters.eq(randomWorld()))
+              .map(Helper::mongoDocumentToWorld)
+              .first();
+    }
+    sendJson(exchange, worlds);
+  }
+}

+ 44 - 0
frameworks/Java/undertow/src/main/java/hello/QueriesSqlHandler.java

@@ -0,0 +1,44 @@
+package hello;
+
+import static hello.Helper.getQueries;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendJson;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.Objects;
+import javax.sql.DataSource;
+
+/**
+ * Handles the multi-query database test using a SQL database.
+ */
+final class QueriesSqlHandler implements HttpHandler {
+  private final DataSource db;
+
+  QueriesSqlHandler(DataSource db) {
+    this.db = Objects.requireNonNull(db);
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) throws Exception {
+    int queries = getQueries(exchange);
+    World[] worlds = new World[queries];
+    try (Connection connection = db.getConnection();
+         PreparedStatement statement =
+             connection.prepareStatement("SELECT * FROM World WHERE id = ?")) {
+      for (int i = 0; i < worlds.length; i++) {
+        statement.setInt(1, randomWorld());
+        try (ResultSet resultSet = statement.executeQuery()) {
+          resultSet.next();
+          int id = resultSet.getInt("id");
+          int randomNumber = resultSet.getInt("randomNumber");
+          worlds[i] = new World(id, randomNumber);
+        }
+      }
+    }
+    sendJson(exchange, worlds);
+  }
+}

+ 85 - 0
frameworks/Java/undertow/src/main/java/hello/UpdatesMongoAsyncHandler.java

@@ -0,0 +1,85 @@
+package hello;
+
+import static hello.Helper.getQueries;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendException;
+import static hello.Helper.sendJson;
+import static hello.Helper.toCompletableFuture;
+
+import com.mongodb.async.client.MongoCollection;
+import com.mongodb.async.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.UpdateOneModel;
+import com.mongodb.client.model.Updates;
+import com.mongodb.client.model.WriteModel;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.IntStream;
+import org.bson.Document;
+import org.bson.conversions.Bson;
+
+/**
+ * Handles the updates test using MongoDB with an asynchronous API.
+ */
+final class UpdatesMongoAsyncHandler implements HttpHandler {
+  private final MongoCollection<Document> worldCollection;
+
+  UpdatesMongoAsyncHandler(MongoDatabase db) {
+    worldCollection = db.getCollection("world");
+  }
+
+  @Override
+  public void handleRequest(HttpServerExchange exchange) {
+    IntStream
+        .range(0, getQueries(exchange))
+        .mapToObj(
+            i -> {
+              CompletableFuture<World> future = new CompletableFuture<>();
+              worldCollection
+                  .find(Filters.eq(randomWorld()))
+                  .map(Helper::mongoDocumentToWorld)
+                  .first(
+                      (world, exception) -> {
+                        if (exception != null) {
+                          future.completeExceptionally(exception);
+                        } else {
+                          future.complete(world);
+                        }
+                      });
+              return future;
+            })
+        .collect(toCompletableFuture())
+        .thenCompose(
+            worlds -> {
+              List<WriteModel<Document>> writes = new ArrayList<>(worlds.size());
+              for (World world : worlds) {
+                world.randomNumber = randomWorld();
+                Bson filter = Filters.eq(world.id);
+                Bson update = Updates.set("randomNumber", world.randomNumber);
+                writes.add(new UpdateOneModel<>(filter, update));
+              }
+              CompletableFuture<List<World>> next = new CompletableFuture<>();
+              worldCollection.bulkWrite(
+                  writes,
+                  (result, exception) -> {
+                    if (exception != null) {
+                      next.completeExceptionally(exception);
+                    } else {
+                      next.complete(worlds);
+                    }
+                  });
+              return next;
+            })
+        .whenComplete(
+            (worlds, exception) -> {
+              if (exception != null) {
+                sendException(exchange, exception);
+              } else {
+                sendJson(exchange, worlds);
+              }
+            });
+  }
+}

+ 33 - 42
frameworks/Java/undertow/src/main/java/hello/UpdatesMongoHandler.java

@@ -1,60 +1,51 @@
 package hello;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.mongodb.BasicDBObject;
-import com.mongodb.DB;
-import com.mongodb.DBObject;
+import static hello.Helper.getQueries;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendJson;
 
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.UpdateOneModel;
+import com.mongodb.client.model.Updates;
+import com.mongodb.client.model.WriteModel;
 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;
+import java.util.ArrayList;
+import java.util.List;
+import org.bson.Document;
+import org.bson.conversions.Bson;
 
 /**
  * Handles the updates test using MongoDB.
  */
 final class UpdatesMongoHandler implements HttpHandler {
-  private final ObjectMapper objectMapper;
-  private final DB database;
+  private final MongoCollection<Document> worldCollection;
 
-  UpdatesMongoHandler(ObjectMapper objectMapper, DB database) {
-    this.objectMapper = Objects.requireNonNull(objectMapper);
-    this.database = Objects.requireNonNull(database);
+  UpdatesMongoHandler(MongoDatabase db) {
+    worldCollection = db.getCollection("world");
   }
 
   @Override
-  public void handleRequest(HttpServerExchange exchange) throws Exception {
-    if (exchange.isInIoThread()) {
-      exchange.dispatch(this);
-      return;
-    }
-    int queries = Helper.getQueries(exchange);
+  public void handleRequest(HttpServerExchange exchange) {
+    int queries = 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);
+    for (int i = 0; i < worlds.length; i++) {
+      worlds[i] =
+          worldCollection
+              .find(Filters.eq(randomWorld()))
+              .map(Helper::mongoDocumentToWorld)
+              .first();
+    }
+    List<WriteModel<Document>> writes = new ArrayList<>(worlds.length);
+    for (World world : worlds) {
+      world.randomNumber = randomWorld();
+      Bson filter = Filters.eq(world.id);
+      Bson update = Updates.set("randomNumber", world.randomNumber);
+      writes.add(new UpdateOneModel<>(filter, update));
     }
-    exchange.getResponseHeaders().put(
-        Headers.CONTENT_TYPE, JSON_UTF8);
-    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+    worldCollection.bulkWrite(writes);
+    sendJson(exchange, worlds);
   }
 }

+ 31 - 52
frameworks/Java/undertow/src/main/java/hello/UpdatesSqlHandler.java

@@ -1,77 +1,56 @@
 package hello;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
+import static hello.Helper.getQueries;
+import static hello.Helper.randomWorld;
+import static hello.Helper.sendJson;
+
 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.Map;
 import java.util.Objects;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Future;
-
-import static hello.HelloWebServer.JSON_UTF8;
+import javax.sql.DataSource;
 
 /**
  * Handles the updates test using a SQL database.
  */
 final class UpdatesSqlHandler implements HttpHandler {
-  private final ObjectMapper objectMapper;
-  private final DataSource database;
+  private final DataSource db;
 
-  UpdatesSqlHandler(ObjectMapper objectMapper, DataSource database) {
-    this.objectMapper = Objects.requireNonNull(objectMapper);
-    this.database = Objects.requireNonNull(database);
+  UpdatesSqlHandler(DataSource db) {
+    this.db = Objects.requireNonNull(db);
   }
 
   @Override
   public void handleRequest(HttpServerExchange exchange) throws Exception {
-    if (exchange.isInIoThread()) {
-      exchange.dispatch(this);
-      return;
-    }
-    int queries = Helper.getQueries(exchange);
+    int queries = getQueries(exchange);
     World[] worlds = new World[queries];
-    try (final Connection connection = database.getConnection()) {
-      Map<Integer, Future<World>> futureWorlds = new ConcurrentHashMap<>();
-      for (int i = 0; i < queries; i++) {
-        futureWorlds.put(i, Helper.EXECUTOR.submit(new Callable<World>() {
-          @Override
-          public World call() throws Exception {
-            World world;
-            try (PreparedStatement update = connection.prepareStatement(
-                "UPDATE World SET randomNumber = ? WHERE id= ?")) {
-              try (PreparedStatement query = connection.prepareStatement(
-                  "SELECT * FROM World WHERE id = ?",
-                  ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
-
-                query.setInt(1, Helper.randomWorld());
-                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();
-              return world;
-            }
+    try (Connection connection = db.getConnection()) {
+      try (PreparedStatement statement =
+               connection.prepareStatement(
+                   "SELECT * FROM World WHERE id = ?")) {
+        for (int i = 0; i < worlds.length; i++) {
+          statement.setInt(1, randomWorld());
+          try (ResultSet resultSet = statement.executeQuery()) {
+            resultSet.next();
+            int id = resultSet.getInt("id");
+            int randomNumber = resultSet.getInt("randomNumber");
+            worlds[i] = new World(id, randomNumber);
           }
-        }));
+        }
       }
-      for (int i = 0; i < queries; i++) {
-        worlds[i] = futureWorlds.get(i).get();
+      try (PreparedStatement statement =
+               connection.prepareStatement(
+                   "UPDATE World SET randomNumber = ? WHERE id = ?")) {
+        for (World world : worlds) {
+          world.randomNumber = randomWorld();
+          statement.setInt(1, world.randomNumber);
+          statement.setInt(2, world.id);
+          statement.executeUpdate();
+        }
       }
     }
-    exchange.getResponseHeaders().put(
-        Headers.CONTENT_TYPE, JSON_UTF8);
-    exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
+    sendJson(exchange, worlds);
   }
 }

+ 0 - 6
frameworks/Java/undertow/src/main/java/hello/World.java

@@ -7,12 +7,6 @@ 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;

+ 11 - 11
frameworks/Java/undertow/src/main/resources/hello/fortunes.mustache

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

+ 16 - 12
frameworks/Java/undertow/src/main/resources/hello/server.properties

@@ -1,14 +1,18 @@
-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
+undertow.port = 8080
+undertow.host = 0.0.0.0
+
+# We use the MySQL configuration recommended in HikariCP's documentation:
+# https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
+mysql.jdbcUrl = jdbc:mysql://TFB-database:3306/hello_world?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048
+mysql.username = 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
+mysql.connections = 32
+
+postgresql.jdbcUrl = jdbc:postgresql://TFB-database:5432/hello_world
+postgresql.username = benchmarkdbuser
 postgresql.password = benchmarkdbpass
-mongodb.host = DATABASE_HOST:27017
-mongodb.name = hello_world
+postgresql.connections = 32
+
+mongodb.host = TFB-database:27017
+mongodb.databaseName = hello_world
+mongodb.connections = 32