HelloWebServer.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. package hello;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import com.github.mustachejava.DefaultMustacheFactory;
  4. import com.github.mustachejava.Mustache;
  5. import com.github.mustachejava.MustacheFactory;
  6. import com.google.common.cache.CacheBuilder;
  7. import com.google.common.cache.CacheLoader;
  8. import com.google.common.cache.LoadingCache;
  9. import com.google.common.net.MediaType;
  10. import com.mongodb.BasicDBObject;
  11. import com.mongodb.DB;
  12. import com.mongodb.DBCursor;
  13. import com.mongodb.DBObject;
  14. import com.mongodb.MongoClient;
  15. import io.undertow.Handlers;
  16. import io.undertow.Undertow;
  17. import io.undertow.server.HttpHandler;
  18. import io.undertow.server.HttpServerExchange;
  19. import io.undertow.util.Headers;
  20. import org.apache.commons.dbcp.ConnectionFactory;
  21. import org.apache.commons.dbcp.DriverManagerConnectionFactory;
  22. import org.apache.commons.dbcp.PoolableConnectionFactory;
  23. import org.apache.commons.dbcp.PoolingDataSource;
  24. import org.apache.commons.pool.impl.GenericObjectPool;
  25. import javax.sql.DataSource;
  26. import java.io.IOException;
  27. import java.io.InputStream;
  28. import java.io.StringWriter;
  29. import java.sql.Connection;
  30. import java.sql.PreparedStatement;
  31. import java.sql.ResultSet;
  32. import java.sql.SQLException;
  33. import java.util.ArrayList;
  34. import java.util.Collections;
  35. import java.util.Deque;
  36. import java.util.List;
  37. import java.util.Properties;
  38. import java.util.concurrent.ExecutionException;
  39. import java.util.concurrent.ThreadLocalRandom;
  40. /**
  41. * An implementation of the TechEmpower benchmark tests using the Undertow web
  42. * server. The only test that truly exercises Undertow in isolation is the
  43. * plaintext test. For the rest, it uses best-of-breed components that are
  44. * expected to perform well. The idea is that using these components enables
  45. * these tests to serve as performance baselines for hypothetical, Undertow-based
  46. * frameworks. For instance, it is unlikely that such frameworks would complete
  47. * the JSON test faster than this will, because this implementation uses
  48. * Undertow and Jackson in the most direct way possible to fulfill the test
  49. * requirements.
  50. */
  51. public final class HelloWebServer {
  52. public static void main(String[] args) throws Exception {
  53. new HelloWebServer();
  54. }
  55. // Helper methods
  56. /**
  57. * Constructs a new SQL data source with the given parameters. Connections
  58. * to this data source are pooled.
  59. *
  60. * @param uri the URI for database connections
  61. * @param user the username for the database
  62. * @param password the password for the database
  63. * @return a new SQL data source
  64. */
  65. private static DataSource newDataSource(String uri,
  66. String user,
  67. String password) {
  68. GenericObjectPool connectionPool = new GenericObjectPool();
  69. connectionPool.setMaxActive(256);
  70. connectionPool.setMaxIdle(256);
  71. ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(
  72. uri, user, password);
  73. //
  74. // This constructor modifies the connection pool, setting its connection
  75. // factory to this. (So despite how it may appear, all of the objects
  76. // declared in this method are incorporated into the returned result.)
  77. //
  78. new PoolableConnectionFactory(
  79. connectionFactory, connectionPool, null, null, false, true);
  80. return new PoolingDataSource(connectionPool);
  81. }
  82. /**
  83. * Returns the value of the "queries" request parameter, which is an integer
  84. * bound between 1 and 500 with a default value of 1.
  85. *
  86. * @param exchange the current HTTP exchange
  87. * @return the value of the "queries" request parameter
  88. */
  89. private static int getQueries(HttpServerExchange exchange) {
  90. Deque<String> values = exchange.getQueryParameters().get("queries");
  91. if (values == null) {
  92. return 1;
  93. }
  94. String textValue = values.peekFirst();
  95. if (textValue == null) {
  96. return 1;
  97. }
  98. try {
  99. int parsedValue = Integer.parseInt(textValue);
  100. return Math.min(500, Math.max(1, parsedValue));
  101. } catch (NumberFormatException e) {
  102. return 1;
  103. }
  104. }
  105. /**
  106. * Returns a random integer that is a suitable value for both the {@code id}
  107. * and {@code randomNumber} properties of a world object.
  108. *
  109. * @return a random world number
  110. */
  111. private static int randomWorld() {
  112. return 1 + ThreadLocalRandom.current().nextInt(10000);
  113. }
  114. // Fields and constructor
  115. private final ObjectMapper objectMapper = new ObjectMapper();
  116. private final MustacheFactory mustacheFactory = new DefaultMustacheFactory();
  117. private final DataSource mysql;
  118. private final DataSource postgresql;
  119. private final DB mongodb;
  120. private final LoadingCache<Integer, World> worldCache;
  121. /**
  122. * Creates and starts a new web server whose configuration is specified in the
  123. * {@code server.properties} file.
  124. *
  125. * @throws IOException if the application properties file cannot be read or
  126. * the Mongo database hostname cannot be resolved
  127. * @throws SQLException if reading from the SQL database (while priming the
  128. * cache) fails
  129. */
  130. public HelloWebServer() throws IOException, SQLException {
  131. Properties properties = new Properties();
  132. try (InputStream in = getClass().getResourceAsStream("server.properties")) {
  133. properties.load(in);
  134. }
  135. mysql = newDataSource(
  136. properties.getProperty("mysql.uri"),
  137. properties.getProperty("mysql.user"),
  138. properties.getProperty("mysql.password"));
  139. postgresql = newDataSource(
  140. properties.getProperty("postgresql.uri"),
  141. properties.getProperty("postgresql.user"),
  142. properties.getProperty("postgresql.password"));
  143. mongodb = new MongoClient(properties.getProperty("mongodb.host"))
  144. .getDB(properties.getProperty("mongodb.name"));
  145. //
  146. // The world cache is primed at startup with all values. It doesn't
  147. // matter which database backs it; they all contain the same information
  148. // and the CacheLoader.load implementation below is never invoked.
  149. //
  150. worldCache = CacheBuilder.newBuilder()
  151. .build(new CacheLoader<Integer, World>() {
  152. @Override
  153. public World load(Integer id) throws Exception {
  154. try (Connection connection = mysql.getConnection();
  155. PreparedStatement statement = connection.prepareStatement(
  156. "SELECT * FROM World WHERE id = ?",
  157. ResultSet.TYPE_FORWARD_ONLY,
  158. ResultSet.CONCUR_READ_ONLY)) {
  159. statement.setInt(1, id);
  160. try (ResultSet resultSet = statement.executeQuery()) {
  161. resultSet.next();
  162. return new World(
  163. resultSet.getInt("id"),
  164. resultSet.getInt("randomNumber"));
  165. }
  166. }
  167. }
  168. });
  169. try (Connection connection = mysql.getConnection();
  170. PreparedStatement statement = connection.prepareStatement(
  171. "SELECT * FROM World",
  172. ResultSet.TYPE_FORWARD_ONLY,
  173. ResultSet.CONCUR_READ_ONLY);
  174. ResultSet resultSet = statement.executeQuery()) {
  175. while (resultSet.next()) {
  176. World world = new World(
  177. resultSet.getInt("id"),
  178. resultSet.getInt("randomNumber"));
  179. worldCache.put(world.id, world);
  180. }
  181. }
  182. Undertow.builder()
  183. .addListener(
  184. Integer.parseInt(properties.getProperty("web.port")),
  185. properties.getProperty("web.host"))
  186. .setBufferSize(1024 * 16)
  187. //
  188. // TODO: Figure out the best values for IoThreads and WorkerThreads.
  189. //
  190. // It is probably some function of the number of processor cores and the
  191. // expected client-side concurrency. The values below seemed to do the
  192. // best out of several that we tried in spot tests on our 8-core i7
  193. // hardware.
  194. //
  195. .setIoThreads(64)
  196. .setWorkerThreads(256)
  197. .setHandler(Handlers.date(Handlers.header(Handlers.path()
  198. .addPath("/json", new HttpHandler() {
  199. @Override
  200. public void handleRequest(HttpServerExchange exchange)
  201. throws Exception {
  202. handleJsonTest(exchange);
  203. }
  204. })
  205. .addPath("/db/mysql", new HttpHandler() {
  206. @Override
  207. public void handleRequest(HttpServerExchange exchange)
  208. throws Exception {
  209. handleDbTest(exchange, mysql);
  210. }
  211. })
  212. .addPath("/db/postgresql", new HttpHandler() {
  213. @Override
  214. public void handleRequest(HttpServerExchange exchange)
  215. throws Exception {
  216. handleDbTest(exchange, postgresql);
  217. }
  218. })
  219. .addPath("/db/mongodb", new HttpHandler() {
  220. @Override
  221. public void handleRequest(HttpServerExchange exchange)
  222. throws Exception {
  223. handleDbTest(exchange, mongodb);
  224. }
  225. })
  226. .addPath("/fortunes/mysql", new HttpHandler() {
  227. @Override
  228. public void handleRequest(HttpServerExchange exchange)
  229. throws Exception {
  230. handleFortunesTest(exchange, mysql);
  231. }
  232. })
  233. .addPath("/fortunes/postgresql", new HttpHandler() {
  234. @Override
  235. public void handleRequest(HttpServerExchange exchange)
  236. throws Exception {
  237. handleFortunesTest(exchange, postgresql);
  238. }
  239. })
  240. .addPath("/fortunes/mongodb", new HttpHandler() {
  241. @Override
  242. public void handleRequest(HttpServerExchange exchange)
  243. throws Exception {
  244. handleFortunesTest(exchange, mongodb);
  245. }
  246. })
  247. .addPath("/updates/mysql", new HttpHandler() {
  248. @Override
  249. public void handleRequest(HttpServerExchange exchange)
  250. throws Exception {
  251. handleUpdatesTest(exchange, mysql);
  252. }
  253. })
  254. .addPath("/updates/postgresql", new HttpHandler() {
  255. @Override
  256. public void handleRequest(HttpServerExchange exchange)
  257. throws Exception {
  258. handleUpdatesTest(exchange, postgresql);
  259. }
  260. })
  261. .addPath("/updates/mongodb", new HttpHandler() {
  262. @Override
  263. public void handleRequest(HttpServerExchange exchange)
  264. throws Exception {
  265. handleUpdatesTest(exchange, mongodb);
  266. }
  267. })
  268. .addPath("/plaintext", new HttpHandler() {
  269. @Override
  270. public void handleRequest(HttpServerExchange exchange)
  271. throws Exception {
  272. handlePlaintextTest(exchange);
  273. }
  274. })
  275. .addPath("/cache", new HttpHandler() {
  276. @Override
  277. public void handleRequest(HttpServerExchange exchange)
  278. throws Exception {
  279. handleCacheTest(exchange);
  280. }
  281. }), Headers.SERVER_STRING, "undertow")))
  282. .build()
  283. .start();
  284. }
  285. // Request handlers
  286. /**
  287. * Completes the JSON test for the given request.
  288. *
  289. * @param exchange the current HTTP exchange
  290. * @throws IOException if JSON encoding fails
  291. */
  292. private void handleJsonTest(HttpServerExchange exchange) throws IOException {
  293. exchange.getResponseHeaders().put(
  294. Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
  295. exchange.getResponseSender().send(
  296. objectMapper.writeValueAsString(
  297. Collections.singletonMap("message", "Hello, World!")));
  298. }
  299. /**
  300. * Completes the database query test for the given request using a SQL
  301. * database. This handler is used for both the single-query and
  302. * multiple-query tests.
  303. *
  304. * @param exchange the current HTTP exchange
  305. * @param database the SQL database
  306. * @throws IOException if JSON encoding fails
  307. * @throws SQLException if reading from the database fails
  308. */
  309. private void handleDbTest(HttpServerExchange exchange, DataSource database)
  310. throws IOException, SQLException {
  311. int queries = getQueries(exchange);
  312. World[] worlds = new World[queries];
  313. try (Connection connection = database.getConnection();
  314. PreparedStatement statement = connection.prepareStatement(
  315. "SELECT * FROM World WHERE id = ?",
  316. ResultSet.TYPE_FORWARD_ONLY,
  317. ResultSet.CONCUR_READ_ONLY)) {
  318. for (int i = 0; i < queries; i++) {
  319. statement.setInt(1, randomWorld());
  320. try (ResultSet resultSet = statement.executeQuery()) {
  321. resultSet.next();
  322. worlds[i] = new World(
  323. resultSet.getInt("id"),
  324. resultSet.getInt("randomNumber"));
  325. }
  326. }
  327. }
  328. exchange.getResponseHeaders().put(
  329. Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
  330. exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
  331. }
  332. /**
  333. * Completes the database query test for the given request using a Mongo
  334. * database. This handler is used for both the single-query and
  335. * multiple-query tests.
  336. *
  337. * @param exchange the current HTTP exchange
  338. * @param database the Mongo database
  339. * @throws IOException if JSON encoding fails
  340. */
  341. private void handleDbTest(HttpServerExchange exchange, DB database)
  342. throws IOException {
  343. int queries = getQueries(exchange);
  344. World[] worlds = new World[queries];
  345. for (int i = 0; i < queries; i++) {
  346. DBObject object = database.getCollection("world").findOne(
  347. new BasicDBObject("id", randomWorld()));
  348. worlds[i] = new World(
  349. //
  350. // The creation script for the Mongo database inserts these numbers as
  351. // JavaScript numbers, which resolve to doubles in Java.
  352. //
  353. ((Number) object.get("id")).intValue(),
  354. ((Number) object.get("randomNumber")).intValue());
  355. }
  356. exchange.getResponseHeaders().put(
  357. Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
  358. exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
  359. }
  360. /**
  361. * Completes the fortunes test for the given request using a SQL database.
  362. *
  363. * @param exchange the current HTTP exchange
  364. * @param database the SQL database
  365. * @throws SQLException if reading from the database fails
  366. */
  367. private void handleFortunesTest(HttpServerExchange exchange,
  368. DataSource database)
  369. throws SQLException {
  370. List<Fortune> fortunes = new ArrayList<>();
  371. try (Connection connection = database.getConnection();
  372. PreparedStatement statement = connection.prepareStatement(
  373. "SELECT * FROM Fortune",
  374. ResultSet.TYPE_FORWARD_ONLY,
  375. ResultSet.CONCUR_READ_ONLY);
  376. ResultSet resultSet = statement.executeQuery()) {
  377. while (resultSet.next()) {
  378. fortunes.add(new Fortune(
  379. resultSet.getInt("id"),
  380. resultSet.getString("message")));
  381. }
  382. }
  383. fortunes.add(new Fortune(0, "Additional fortune added at request time."));
  384. Collections.sort(fortunes);
  385. Mustache mustache = mustacheFactory.compile("hello/fortunes.mustache");
  386. StringWriter writer = new StringWriter();
  387. mustache.execute(writer, fortunes);
  388. exchange.getResponseHeaders().put(
  389. Headers.CONTENT_TYPE, MediaType.HTML_UTF_8.toString());
  390. exchange.getResponseSender().send(writer.toString());
  391. }
  392. /**
  393. * Completes the fortunes test for the given request using a Mongo database.
  394. *
  395. * @param exchange the current HTTP exchange
  396. * @param database the Mongo database
  397. */
  398. private void handleFortunesTest(HttpServerExchange exchange, DB database) {
  399. List<Fortune> fortunes = new ArrayList<>();
  400. DBCursor cursor = database.getCollection("fortune").find();
  401. while (cursor.hasNext()) {
  402. DBObject object = cursor.next();
  403. fortunes.add(new Fortune(
  404. ((Number) object.get("id")).intValue(),
  405. (String) object.get("message")));
  406. }
  407. fortunes.add(new Fortune(0, "Additional fortune added at request time."));
  408. Collections.sort(fortunes);
  409. Mustache mustache = mustacheFactory.compile("hello/fortunes.mustache");
  410. StringWriter writer = new StringWriter();
  411. mustache.execute(writer, fortunes);
  412. exchange.getResponseHeaders().put(
  413. Headers.CONTENT_TYPE, MediaType.HTML_UTF_8.toString());
  414. exchange.getResponseSender().send(writer.toString());
  415. }
  416. /**
  417. * Completes the database update test for the given request using a SQL
  418. * database.
  419. *
  420. * @param exchange the current HTTP exchange
  421. * @param database the SQL database
  422. * @throws IOException if JSON encoding fails
  423. * @throws SQLException if reading from or writing to the database fails
  424. */
  425. private void handleUpdatesTest(HttpServerExchange exchange,
  426. DataSource database)
  427. throws IOException, SQLException {
  428. int queries = getQueries(exchange);
  429. World[] worlds = new World[queries];
  430. try (Connection connection = database.getConnection();
  431. PreparedStatement query = connection.prepareStatement(
  432. "SELECT * FROM World WHERE id = ?",
  433. ResultSet.TYPE_FORWARD_ONLY,
  434. ResultSet.CONCUR_READ_ONLY);
  435. PreparedStatement update = connection.prepareStatement(
  436. "UPDATE World SET randomNumber = ? WHERE id= ?")) {
  437. for (int i = 0; i < queries; i++) {
  438. query.setInt(1, randomWorld());
  439. World world;
  440. try (ResultSet resultSet = query.executeQuery()) {
  441. resultSet.next();
  442. world = new World(
  443. resultSet.getInt("id"),
  444. resultSet.getInt("randomNumber"));
  445. }
  446. world.randomNumber = randomWorld();
  447. update.setInt(1, world.randomNumber);
  448. update.setInt(2, world.id);
  449. update.executeUpdate();
  450. worlds[i] = world;
  451. }
  452. }
  453. exchange.getResponseHeaders().put(
  454. Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
  455. exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
  456. }
  457. /**
  458. * Completes the database update test for the given request using a Mongo
  459. * database.
  460. *
  461. * @param exchange the current HTTP exchange
  462. * @param database the Mongo database
  463. * @throws IOException if JSON encoding fails
  464. */
  465. private void handleUpdatesTest(HttpServerExchange exchange, DB database)
  466. throws IOException {
  467. int queries = getQueries(exchange);
  468. World[] worlds = new World[queries];
  469. for (int i = 0; i < queries; i++) {
  470. int id = randomWorld();
  471. DBObject key = new BasicDBObject("id", id);
  472. //
  473. // The requirements for the test dictate that we must fetch the World
  474. // object from the data store and read its randomNumber field, even though
  475. // we could technically avoid doing either of those things and still
  476. // produce the correct output and side effects.
  477. //
  478. DBObject object = database.getCollection("world").findOne(key);
  479. int oldRandomNumber = ((Number) object.get("randomNumber")).intValue();
  480. int newRandomNumber = randomWorld();
  481. object.put("randomNumber", newRandomNumber);
  482. database.getCollection("world").update(key, object);
  483. worlds[i] = new World(id, newRandomNumber);
  484. }
  485. exchange.getResponseHeaders().put(
  486. Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
  487. exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
  488. }
  489. /**
  490. * Completes the plaintext test forthe given request.
  491. *
  492. * @param exchange the current HTTP exchange
  493. */
  494. private void handlePlaintextTest(HttpServerExchange exchange) {
  495. exchange.getResponseHeaders().put(
  496. Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString());
  497. exchange.getResponseSender().send("Hello, World!");
  498. }
  499. /**
  500. * Completes the cache test for the given request.
  501. *
  502. * @param exchange the current HTTP exchange
  503. * @throws ExecutionException the reading from the cache fails
  504. * @throws IOException if JSON encoding fails
  505. */
  506. private void handleCacheTest(HttpServerExchange exchange)
  507. throws ExecutionException, IOException {
  508. int queries = getQueries(exchange);
  509. World[] worlds = new World[queries];
  510. for (int i = 0; i < queries; i++) {
  511. worlds[i] = worldCache.get(randomWorld());
  512. }
  513. exchange.getResponseHeaders().put(
  514. Headers.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
  515. exchange.getResponseSender().send(objectMapper.writeValueAsString(worlds));
  516. }
  517. }