|
@@ -1,10 +1,6 @@
|
|
|
package io.vertx.benchmark;
|
|
|
|
|
|
-import com.github.susom.database.Config;
|
|
|
-import com.github.susom.database.ConfigFrom;
|
|
|
-import com.github.susom.database.DatabaseProviderVertx;
|
|
|
-import com.github.susom.database.DatabaseProviderVertx.Builder;
|
|
|
-import com.github.susom.database.SqlInsert;
|
|
|
+import com.fasterxml.jackson.module.blackbird.BlackbirdModule;
|
|
|
import io.vertx.benchmark.model.Fortune;
|
|
|
import io.vertx.benchmark.model.Message;
|
|
|
import io.vertx.benchmark.model.World;
|
|
@@ -12,7 +8,7 @@ import io.vertx.core.*;
|
|
|
import io.vertx.core.http.HttpHeaders;
|
|
|
import io.vertx.core.json.Json;
|
|
|
import io.vertx.core.json.JsonObject;
|
|
|
-import io.vertx.ext.mongo.MongoClient;
|
|
|
+import io.vertx.core.json.jackson.DatabindCodec;
|
|
|
import io.vertx.ext.web.Router;
|
|
|
import io.vertx.ext.web.RoutingContext;
|
|
|
import io.vertx.ext.web.templ.rocker.RockerTemplateEngine;
|
|
@@ -28,155 +24,9 @@ import static io.vertx.benchmark.Helper.randomWorld;
|
|
|
|
|
|
public class App extends AbstractVerticle {
|
|
|
|
|
|
- /**
|
|
|
- * MongoDB implementation
|
|
|
- */
|
|
|
- private final class MongoDBBenchmark {
|
|
|
- private final JsonObject FIELDS = new JsonObject().put("_id", 0);
|
|
|
-
|
|
|
- private final MongoClient database;
|
|
|
- // In order to use a template we first need to create an engine
|
|
|
- private final RockerTemplateEngine engine;
|
|
|
-
|
|
|
- public MongoDBBenchmark(Vertx vertx, JsonObject config) {
|
|
|
- final JsonObject mongoConfig = config.copy();
|
|
|
-
|
|
|
- // mongo is configured without credentials
|
|
|
- mongoConfig.remove("username");
|
|
|
- mongoConfig.remove("password");
|
|
|
-
|
|
|
- this.database = MongoClient.createShared(vertx, mongoConfig);
|
|
|
- this.engine = RockerTemplateEngine.create();
|
|
|
- }
|
|
|
-
|
|
|
- public final void dbHandler(final RoutingContext ctx) {
|
|
|
- database.findOne("world", new JsonObject().put("_id", randomWorld()), FIELDS, findOne -> {
|
|
|
- if (findOne.failed()) {
|
|
|
- ctx.fail(findOne.cause());
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
|
- .end(Json.encodeToBuffer(new World(findOne.result())));
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- public final void queriesHandler(final RoutingContext ctx) {
|
|
|
- final int queries = Helper.getQueries(ctx.request());
|
|
|
-
|
|
|
- final World[] worlds = new World[queries];
|
|
|
-
|
|
|
- new Handler<Integer>() {
|
|
|
- @Override
|
|
|
- public void handle(Integer idx) {
|
|
|
- if (idx == queries) {
|
|
|
- // stop condition
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
|
- .end(Json.encodeToBuffer(worlds));
|
|
|
-
|
|
|
- } else {
|
|
|
-
|
|
|
- final Handler<Integer> self = this;
|
|
|
-
|
|
|
- database.findOne("world", new JsonObject().put("_id", randomWorld()), FIELDS, findOne -> {
|
|
|
- if (findOne.failed()) {
|
|
|
- ctx.fail(findOne.cause());
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- worlds[idx] = new World(findOne.result());
|
|
|
- self.handle(idx + 1);
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- }.handle(0);
|
|
|
- }
|
|
|
-
|
|
|
- public final void fortunesHandler(final RoutingContext ctx) {
|
|
|
- final List<Fortune> fortunes = new LinkedList<>();
|
|
|
-
|
|
|
- database.find("fortune", new JsonObject(), find -> {
|
|
|
- if (find.failed()) {
|
|
|
- ctx.fail(find.cause());
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- for (JsonObject document : find.result()) {
|
|
|
- fortunes.add(new Fortune(document));
|
|
|
- }
|
|
|
-
|
|
|
- fortunes.add(new Fortune(0, "Additional fortune added at request time."));
|
|
|
- Collections.sort(fortunes);
|
|
|
-
|
|
|
- ctx.put("fortunes", fortunes);
|
|
|
-
|
|
|
- // and now delegate to the engine to render it.
|
|
|
- engine.render(ctx.data(), "templates/Fortunes.rocker.html", res -> {
|
|
|
- if (res.succeeded()) {
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
|
|
|
- .end(res.result());
|
|
|
- } else {
|
|
|
- ctx.fail(res.cause());
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- public final void updateHandler(final RoutingContext ctx) {
|
|
|
- final int queries = Helper.getQueries(ctx.request());
|
|
|
- final World[] worlds = new World[queries];
|
|
|
-
|
|
|
- new Handler<Integer>() {
|
|
|
- @Override
|
|
|
- public void handle(Integer idx) {
|
|
|
- if (idx == queries) {
|
|
|
- // stop condition
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
|
- .end(Json.encodeToBuffer(worlds));
|
|
|
-
|
|
|
- } else {
|
|
|
-
|
|
|
- final Handler<Integer> self = this;
|
|
|
-
|
|
|
- final int id = randomWorld();
|
|
|
-
|
|
|
- final JsonObject query = new JsonObject().put("_id", id);
|
|
|
-
|
|
|
- database.findOne("world", query, FIELDS, findOne -> {
|
|
|
- if (findOne.failed()) {
|
|
|
- ctx.fail(findOne.cause());
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- final int newRandomNumber = randomWorld();
|
|
|
-
|
|
|
- database.update("world", query, new JsonObject().put("$set", new JsonObject().put("randomNumber", newRandomNumber)), update -> {
|
|
|
- if (update.failed()) {
|
|
|
- ctx.fail(update.cause());
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- worlds[idx] = new World(id, newRandomNumber);
|
|
|
- self.handle(idx + 1);
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
- }.handle(0);
|
|
|
- }
|
|
|
+ static {
|
|
|
+ DatabindCodec.mapper().registerModule(new BlackbirdModule());
|
|
|
+ DatabindCodec.prettyMapper().registerModule(new BlackbirdModule());
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -200,13 +50,14 @@ public class App extends AbstractVerticle {
|
|
|
.setPort(config.getInteger("port", 5432))
|
|
|
.setUser(config.getString("username"))
|
|
|
.setPassword(config.getString("password"))
|
|
|
- .setDatabase(config.getString("database"));
|
|
|
+ .setDatabase(config.getString("database"))
|
|
|
+ .setPipeliningLimit(100_000); // Large pipelining means less flushing and we use a single connection anyway;
|
|
|
|
|
|
client = PgPool.pool(vertx, options, new PoolOptions().setMaxSize(4));
|
|
|
this.engine = RockerTemplateEngine.create();
|
|
|
}
|
|
|
|
|
|
- public final void dbHandler(final RoutingContext ctx) {
|
|
|
+ public void dbHandler(final RoutingContext ctx) {
|
|
|
client
|
|
|
.preparedQuery(SELECT_WORLD)
|
|
|
.execute(Tuple.of(randomWorld()), res -> {
|
|
@@ -231,7 +82,7 @@ public class App extends AbstractVerticle {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- public final void queriesHandler(final RoutingContext ctx) {
|
|
|
+ public void queriesHandler(final RoutingContext ctx) {
|
|
|
|
|
|
final int queries = Helper.getQueries(ctx.request());
|
|
|
final World[] worlds = new World[queries];
|
|
@@ -264,7 +115,7 @@ public class App extends AbstractVerticle {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public final void fortunesHandler(final RoutingContext ctx) {
|
|
|
+ public void fortunesHandler(final RoutingContext ctx) {
|
|
|
|
|
|
client.preparedQuery(SELECT_FORTUNE).execute(ar -> {
|
|
|
if (ar.succeeded()) {
|
|
@@ -304,7 +155,7 @@ public class App extends AbstractVerticle {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- public final void updateHandler(final RoutingContext ctx) {
|
|
|
+ public void updateHandler(final RoutingContext ctx) {
|
|
|
|
|
|
final int queries = Helper.getQueries(ctx.request());
|
|
|
final World[] worlds = new World[queries];
|
|
@@ -351,148 +202,6 @@ public class App extends AbstractVerticle {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Implementation using com.github.susom:database library and standard JDBC driver.
|
|
|
- */
|
|
|
- private final class DatabaseSqlBenchmark {
|
|
|
- private final Builder dbBuilder;
|
|
|
-
|
|
|
- // In order to use a template we first need to create an engine
|
|
|
- private final RockerTemplateEngine engine;
|
|
|
-
|
|
|
- DatabaseSqlBenchmark(Vertx vertx, JsonObject jsonConfig) {
|
|
|
- Config config = ConfigFrom.firstOf().custom(jsonConfig::getString)
|
|
|
- .rename("username", "database.user")
|
|
|
- .rename("password", "database.password")
|
|
|
- .rename("maxPoolSize", "database.pool.size")
|
|
|
- .value("database.url", "jdbc:postgresql://" + jsonConfig.getString("host") + "/" + jsonConfig.getString("database"))
|
|
|
- .get();
|
|
|
- dbBuilder = DatabaseProviderVertx.pooledBuilder(vertx, config);
|
|
|
- engine = RockerTemplateEngine.create();
|
|
|
- }
|
|
|
-
|
|
|
- void dbHandler(final RoutingContext ctx) {
|
|
|
- dbBuilder.transactAsync(dbs ->
|
|
|
- dbs.get().toSelect("SELECT id, randomnumber from WORLD where id = ?")
|
|
|
- .argInteger(randomWorld())
|
|
|
- .queryFirstOrNull(row -> new World(row.getIntegerOrZero(), row.getIntegerOrZero()))
|
|
|
- , call -> {
|
|
|
- if (call.succeeded()) {
|
|
|
- if (call.result() == null) {
|
|
|
- ctx.fail(404);
|
|
|
- } else {
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
|
- .end(Json.encode(call.result()));
|
|
|
- }
|
|
|
- } else {
|
|
|
- ctx.fail(call.cause());
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- void queriesHandler(final RoutingContext ctx) {
|
|
|
- int queries = Helper.getQueries(ctx.request());
|
|
|
-
|
|
|
- dbBuilder.transactAsync(dbs -> {
|
|
|
- List<World> worlds = new ArrayList<>();
|
|
|
- for (int i = 1; i <= queries; i++) {
|
|
|
- dbs.get()
|
|
|
- .toSelect("SELECT id, randomnumber from WORLD where id = ?")
|
|
|
- .argInteger(randomWorld())
|
|
|
- .queryFirstOrNull(row -> worlds.add(new World(row.getIntegerOrZero(), row.getIntegerOrZero())));
|
|
|
- }
|
|
|
- return worlds;
|
|
|
- }, call -> {
|
|
|
- if (call.succeeded()) {
|
|
|
- if (call.result() == null) {
|
|
|
- ctx.fail(404);
|
|
|
- } else {
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
|
- .end(Json.encodeToBuffer(call.result()));
|
|
|
- }
|
|
|
- } else {
|
|
|
- ctx.fail(call.cause());
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- final void fortunesHandler(final RoutingContext ctx) {
|
|
|
- dbBuilder.transactAsync(dbs -> {
|
|
|
- List<Fortune> fortunes = dbs.get()
|
|
|
- .toSelect("SELECT id, message from FORTUNE")
|
|
|
- .queryMany(row -> new Fortune(row.getIntegerOrZero(), row.getStringOrEmpty()));
|
|
|
- fortunes.add(new Fortune(0, "Additional fortune added at request time."));
|
|
|
- Collections.sort(fortunes);
|
|
|
- return fortunes;
|
|
|
- }, call -> {
|
|
|
- if (call.succeeded()) {
|
|
|
- if (call.result() == null || call.result().isEmpty()) {
|
|
|
- ctx.fail(404);
|
|
|
- } else {
|
|
|
- ctx.put("fortunes", call.result());
|
|
|
- engine.render(ctx.data(), "templates/Fortunes.rocker.html", res -> {
|
|
|
- if (res.succeeded()) {
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
|
|
|
- .end(res.result());
|
|
|
- } else {
|
|
|
- ctx.fail(res.cause());
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- } else {
|
|
|
- ctx.fail(call.cause());
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- void updateHandler(final RoutingContext ctx) {
|
|
|
- int queries = Helper.getQueries(ctx.request());
|
|
|
-
|
|
|
- dbBuilder.transactAsync(dbs -> {
|
|
|
- List<World> worlds = new ArrayList<>();
|
|
|
- // Haven't implemented batching yet on toUpdate(), so hijack toInsert() as work-around
|
|
|
- SqlInsert batchUpdate = dbs.get().toInsert("UPDATE WORLD SET randomnumber = ? WHERE id = ?");
|
|
|
- for (int i = 1; i <= queries; i++) {
|
|
|
- World oldWorld = dbs.get()
|
|
|
- .toSelect("SELECT id, randomnumber from WORLD where id = ?")
|
|
|
- .argInteger(randomWorld())
|
|
|
- .queryFirstOrNull(row -> new World(row.getIntegerOrZero(), row.getIntegerOrZero()));
|
|
|
- if (oldWorld == null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- World newWorld = new World(oldWorld.getId(), randomWorld());
|
|
|
- worlds.add(newWorld);
|
|
|
- batchUpdate.argInteger(newWorld.getRandomNumber()).argInteger(newWorld.getId()).batch();
|
|
|
- }
|
|
|
- batchUpdate.insertBatchUnchecked();
|
|
|
- return worlds;
|
|
|
- }, call -> {
|
|
|
- if (call.succeeded()) {
|
|
|
- if (call.result() == null) {
|
|
|
- ctx.fail(404);
|
|
|
- } else {
|
|
|
- ctx.response()
|
|
|
- .putHeader(HttpHeaders.SERVER, SERVER)
|
|
|
- .putHeader(HttpHeaders.DATE, date)
|
|
|
- .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
|
|
- .end(Json.encodeToBuffer(call.result()));
|
|
|
- }
|
|
|
- } else {
|
|
|
- ctx.fail(call.cause());
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
private static final String SERVER = "vertx-web";
|
|
|
private String date;
|
|
|
|
|
@@ -504,9 +213,7 @@ public class App extends AbstractVerticle {
|
|
|
// refresh the value as a periodic task
|
|
|
vertx.setPeriodic(1000, handler -> date = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now()));
|
|
|
|
|
|
- final MongoDBBenchmark mongoDBBenchmark = new MongoDBBenchmark(vertx, config());
|
|
|
final PgClientBenchmark pgClientBenchmark= new PgClientBenchmark(vertx, config());
|
|
|
- final DatabaseSqlBenchmark databaseSqlBenchmark = new DatabaseSqlBenchmark(vertx, config());
|
|
|
|
|
|
/*
|
|
|
* This test exercises the framework fundamentals including keep-alive support, request routing, request header
|
|
@@ -524,35 +231,27 @@ public class App extends AbstractVerticle {
|
|
|
* This test exercises the framework's object-relational mapper (ORM), random number generator, database driver,
|
|
|
* and database connection pool.
|
|
|
*/
|
|
|
- app.get("/mongo/db").handler(mongoDBBenchmark::dbHandler);
|
|
|
- app.get("/psql/db").handler(pgClientBenchmark::dbHandler);
|
|
|
- app.get("/dbpsql/db").handler(databaseSqlBenchmark::dbHandler);
|
|
|
+ app.get("/db").handler(pgClientBenchmark::dbHandler);
|
|
|
|
|
|
/*
|
|
|
* This test is a variation of Test #2 and also uses the World table. Multiple rows are fetched to more dramatically
|
|
|
* punish the database driver and connection pool. At the highest queries-per-request tested (20), this test
|
|
|
* demonstrates all frameworks' convergence toward zero requests-per-second as database activity increases.
|
|
|
*/
|
|
|
- app.get("/mongo/queries").handler(mongoDBBenchmark::queriesHandler);
|
|
|
- app.get("/psql/queries").handler(pgClientBenchmark::queriesHandler);
|
|
|
- app.get("/dbpsql/queries").handler(databaseSqlBenchmark::queriesHandler);
|
|
|
+ app.get("/queries").handler(pgClientBenchmark::queriesHandler);
|
|
|
|
|
|
/*
|
|
|
* This test exercises the ORM, database connectivity, dynamic-size collections, sorting, server-side templates,
|
|
|
* XSS countermeasures, and character encoding.
|
|
|
*/
|
|
|
- app.get("/mongo/fortunes").handler(mongoDBBenchmark::fortunesHandler);
|
|
|
- app.get("/psql/fortunes").handler(pgClientBenchmark::fortunesHandler);
|
|
|
- app.get("/dbpsql/fortunes").handler(databaseSqlBenchmark::fortunesHandler);
|
|
|
+ app.get("/fortunes").handler(pgClientBenchmark::fortunesHandler);
|
|
|
|
|
|
/*
|
|
|
* This test is a variation of Test #3 that exercises the ORM's persistence of objects and the database driver's
|
|
|
* performance at running UPDATE statements or similar. The spirit of this test is to exercise a variable number of
|
|
|
* read-then-write style database operations.
|
|
|
*/
|
|
|
- app.route("/mongo/update").handler(mongoDBBenchmark::updateHandler);
|
|
|
- app.route("/psql/update").handler(pgClientBenchmark::updateHandler);
|
|
|
- app.route("/dbpsql/update").handler(databaseSqlBenchmark::updateHandler);
|
|
|
+ app.route("/update").handler(pgClientBenchmark::updateHandler);
|
|
|
|
|
|
/*
|
|
|
* This test is an exercise of the request-routing fundamentals only, designed to demonstrate the capacity of
|