|
@@ -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;
|
|
|
}
|
|
|
}
|