Browse Source

Update armeria to latest version, use more idiomatic armeria code, and ensure DB calls are not on the event loop.

Anuraag Agrawal 5 years ago
parent
commit
fd940da855

+ 1 - 1
frameworks/Java/armeria/pom.xml

@@ -16,7 +16,7 @@
     <maven.compiler.target>11</maven.compiler.target>
 
     <!-- Dependency versions -->
-    <armeria.version>0.86.0</armeria.version>
+    <armeria.version>0.93.0</armeria.version>
   </properties>
 
   <dependencies>

+ 16 - 4
frameworks/Java/armeria/src/main/java/hello/App.java

@@ -1,12 +1,17 @@
 package hello;
 
-import hello.services.HelloService;
-import hello.services.PostgresDbService;
-import hello.services.PostgresFortunesService;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 
+import com.linecorp.armeria.common.HttpHeaderNames;
 import com.linecorp.armeria.server.Server;
 import com.linecorp.armeria.server.ServerBuilder;
 
+import hello.services.HelloService;
+import hello.services.PostgresDbService;
+import hello.services.PostgresFortunesService;
+
 public final class App {
   public static void main(String[] args) {
     ServerBuilder sb = new ServerBuilder();
@@ -14,7 +19,14 @@ public final class App {
     sb.http(8080)
       .annotatedService("/", new HelloService())
       .annotatedService("/", new PostgresDbService())
-      .annotatedService("/", new PostgresFortunesService());
+      .annotatedService("/", new PostgresFortunesService())
+      .decorator((delegate, ctx, req) -> {
+        ctx.addAdditionalResponseHeader(HttpHeaderNames.SERVER, "armeria");
+        ctx.addAdditionalResponseHeader(HttpHeaderNames.DATE,
+                                        DateTimeFormatter.RFC_1123_DATE_TIME.format(
+                                                ZonedDateTime.now(ZoneOffset.UTC)));
+        return delegate.serve(ctx, req);
+      });
 
     Server server = sb.build();
     server.start().join();

+ 0 - 19
frameworks/Java/armeria/src/main/java/hello/helpers/HttpHeadersHelper.java

@@ -1,19 +0,0 @@
-package hello.helpers;
-
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-
-import com.linecorp.armeria.common.HttpHeaderNames;
-import com.linecorp.armeria.common.HttpStatus;
-import com.linecorp.armeria.common.MediaType;
-import com.linecorp.armeria.common.ResponseHeaders;
-
-public class HttpHeadersHelper {
-  public static ResponseHeaders getHttpHeader(MediaType mediaType) {
-    return ResponseHeaders.of(HttpStatus.OK,
-    		HttpHeaderNames.CONTENT_TYPE, mediaType,
-    		HttpHeaderNames.SERVER, "armeria",
-    		HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)));
-  }
-}

+ 8 - 16
frameworks/Java/armeria/src/main/java/hello/services/HelloService.java

@@ -1,37 +1,29 @@
 package hello.services;
 
-import hello.helpers.HttpHeadersHelper;
-import hello.models.Message;
-
 import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.linecorp.armeria.common.HttpData;
 import com.linecorp.armeria.common.HttpResponse;
+import com.linecorp.armeria.common.HttpStatus;
 import com.linecorp.armeria.common.MediaType;
 import com.linecorp.armeria.server.annotation.Get;
 import com.linecorp.armeria.server.annotation.ProducesJson;
 
+import hello.models.Message;
+
 public class HelloService {
   private static final byte[] PLAINTEXT =
       "Hello, World!".getBytes(StandardCharsets.UTF_8);
-  private static final ObjectMapper MAPPER = new ObjectMapper();
 
   @Get("/plaintext")
   public HttpResponse plaintext() {
-    return HttpResponse.of(
-    	HttpHeadersHelper.getHttpHeader(MediaType.PLAIN_TEXT_UTF_8),
-        HttpData.of(PLAINTEXT)
-        );
+    return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, PLAINTEXT);
   }
 
   @Get("/json")
   @ProducesJson
-  public HttpResponse json() throws JsonProcessingException {
-    return HttpResponse.of(
-    	HttpHeadersHelper.getHttpHeader(MediaType.JSON),
-        HttpData.of(MAPPER.writeValueAsBytes(new Message("Hello, World!")))
-        );
+  // TODO(anuraaga): Change return type to Message after https://github.com/line/armeria/issues/2078
+  public CompletableFuture<Message> json() {
+    return CompletableFuture.completedFuture(new Message("Hello, World!"));
   }
 }

+ 89 - 87
frameworks/Java/armeria/src/main/java/hello/services/PostgresDbService.java

@@ -1,29 +1,26 @@
 package hello.services;
 
-import hello.helpers.HttpHeadersHelper;
-import hello.helpers.PostgresDbHelper;
-import hello.models.World;
-
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ThreadLocalRandom;
 
 import javax.sql.DataSource;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.linecorp.armeria.common.HttpData;
-import com.linecorp.armeria.common.HttpResponse;
-import com.linecorp.armeria.common.MediaType;
+import com.zaxxer.hikari.HikariDataSource;
+
+import com.linecorp.armeria.server.ServiceRequestContext;
 import com.linecorp.armeria.server.annotation.Default;
 import com.linecorp.armeria.server.annotation.Get;
 import com.linecorp.armeria.server.annotation.Param;
-import com.zaxxer.hikari.HikariDataSource;
+import com.linecorp.armeria.server.annotation.ProducesJson;
+
+import hello.helpers.PostgresDbHelper;
+import hello.models.World;
 
 public class PostgresDbService {
-  private static final ObjectMapper MAPPER = new ObjectMapper();
 
   private static final String SELECT_QUERY = "SELECT * FROM world WHERE id = ?";
   private static final String UPDATE_QUERY =
@@ -36,37 +33,26 @@ public class PostgresDbService {
   }
 
   @Get("/db")
-  public HttpResponse db() throws Exception {
-    return HttpResponse.of(
-    	HttpHeadersHelper.getHttpHeader(MediaType.JSON),
-        HttpData.of(MAPPER.writeValueAsBytes(getWorld(getRandomNumber())))
-        );
+  @ProducesJson
+  public CompletableFuture<World> db() throws Exception {
+    return getWorld(getRandomNumber());
   }
 
   // need to use regex as /queries/{count} doesn't work when count is null
   @Get("regex:^/queries/(?<count>.*)$")
-  public HttpResponse queries(
+  @ProducesJson
+  public CompletableFuture<World[]> queries(
       @Param("count")
-      @Default("")
-          String count) throws JsonProcessingException, SQLException {
-    return HttpResponse.of(
-        HttpHeadersHelper.getHttpHeader(MediaType.JSON),
-        HttpData.of(
-            MAPPER.writeValueAsBytes(getWorlds(getSanitizedCount(count))))
-        );
+      @Default("") String count) {
+    return getWorlds(getSanitizedCount(count));
   }
 
   @Get("regex:^/updates/(?<count>.*)$")
-  public HttpResponse update(
+  @ProducesJson
+  public CompletableFuture<World[]> update(
       @Param("count")
-      @Default("")
-          String count) throws JsonProcessingException, SQLException {
-    return HttpResponse.of(
-        HttpHeadersHelper.getHttpHeader(MediaType.JSON),
-        HttpData.of(
-            MAPPER.writeValueAsBytes(
-                getUpdatedWorlds(getSanitizedCount(count))))
-        );
+      @Default("") String count) {
+    return getUpdatedWorlds(getSanitizedCount(count));
   }
 
   private static int getRandomNumber() {
@@ -88,71 +74,87 @@ public class PostgresDbService {
     }
   }
 
-  private World getWorld(int number) throws SQLException {
-    try (final Connection connection = dataSource.getConnection();
-         final PreparedStatement statement =
-             connection.prepareStatement(SELECT_QUERY)) {
-
-      statement.setInt(1, number);
-
-      try (final ResultSet resultSet = statement.executeQuery()) {
-        resultSet.next();
-        return new World(resultSet.getInt(1), resultSet.getInt(2));
-      }
-    }
+  private CompletableFuture<World> getWorld(int number) {
+    return CompletableFuture.supplyAsync(
+        () -> {
+          try (final Connection connection = dataSource.getConnection();
+               final PreparedStatement statement =
+                   connection.prepareStatement(SELECT_QUERY)) {
+
+            statement.setInt(1, number);
+
+            try (final ResultSet resultSet = statement.executeQuery()) {
+              resultSet.next();
+              return new World(resultSet.getInt(1), resultSet.getInt(2));
+            }
+          } catch (SQLException e) {
+            throw new IllegalStateException("Database error", e);
+          }
+        }, ServiceRequestContext.current().blockingTaskExecutor());
   }
 
-  private World[] getWorlds(int count) throws SQLException {
-    World[] worlds = new World[count];
-
-    try (final Connection connection = dataSource.getConnection()) {
-      for (int i = 0; i < count; i++) {
-        final int id = getRandomNumber();
-
-        try (final PreparedStatement statement =
-                 connection.prepareStatement(SELECT_QUERY)) {
-          statement.setInt(1, id);
-
-          try (final ResultSet resultSet = statement.executeQuery()) {
-            resultSet.next();
-            worlds[i] = new World(id, resultSet.getInt(2));
+  private CompletableFuture<World[]> getWorlds(int count) {
+    return CompletableFuture.supplyAsync(
+        () -> {
+          World[] worlds = new World[count];
+
+          try (final Connection connection = dataSource.getConnection()) {
+            for (int i = 0; i < count; i++) {
+              final int id = getRandomNumber();
+
+              try (final PreparedStatement statement =
+                       connection.prepareStatement(SELECT_QUERY)) {
+                statement.setInt(1, id);
+
+                try (final ResultSet resultSet = statement.executeQuery()) {
+                  resultSet.next();
+                  worlds[i] = new World(id, resultSet.getInt(2));
+                }
+              }
+            }
+          } catch (SQLException e) {
+            throw new IllegalStateException("Database error", e);
           }
-        }
-      }
-    }
-    return worlds;
+          return worlds;
+        }, ServiceRequestContext.current().blockingTaskExecutor());
   }
 
-  private World[] getUpdatedWorlds(int count) throws SQLException {
-    World[] worlds = new World[count];
+  private CompletableFuture<World[]> getUpdatedWorlds(int count) {
+    return CompletableFuture.supplyAsync(
+        () -> {
+
+          World[] worlds = new World[count];
 
-    try (final Connection connection = dataSource.getConnection()) {
-      for (int i = 0; i < count; i++) {
-        final int id = getRandomNumber();
-        final int randomNumber = getRandomNumber();
+          try (final Connection connection = dataSource.getConnection()) {
+            for (int i = 0; i < count; i++) {
+              final int id = getRandomNumber();
+              final int randomNumber = getRandomNumber();
 
-        try (final PreparedStatement select =
-                 connection.prepareStatement(SELECT_QUERY);
-             final PreparedStatement update =
-                 connection.prepareStatement(UPDATE_QUERY)) {
+              try (final PreparedStatement select =
+                       connection.prepareStatement(SELECT_QUERY);
+                   final PreparedStatement update =
+                       connection.prepareStatement(UPDATE_QUERY)) {
 
-          // get
-          select.setInt(1, id);
+                // get
+                select.setInt(1, id);
 
-          try (final ResultSet set = select.executeQuery()) {
-            set.next();
+                try (final ResultSet set = select.executeQuery()) {
+                  set.next();
 
-            // update
-            update.setInt(1, randomNumber);
-            update.setInt(2, id);
-            update.execute();
+                  // update
+                  update.setInt(1, randomNumber);
+                  update.setInt(2, id);
+                  update.execute();
 
-            worlds[i] = new World(id, set.getInt(2));
-            worlds[i].randomNumber = randomNumber;
+                  worlds[i] = new World(id, set.getInt(2));
+                  worlds[i].randomNumber = randomNumber;
+                }
+              }
+            }
+          } catch (SQLException e) {
+            throw new IllegalStateException("Database error", e);
           }
-        }
-      }
-    }
-    return worlds;
+          return worlds;
+        }, ServiceRequestContext.current().blockingTaskExecutor());
   }
 }

+ 58 - 46
frameworks/Java/armeria/src/main/java/hello/services/PostgresFortunesService.java

@@ -1,32 +1,36 @@
 package hello.services;
 
-import com.zaxxer.hikari.HikariDataSource;
-
-import hello.helpers.PostgresDbHelper;
-import hello.models.Fortune;
-import hello.helpers.HttpHeadersHelper;
-
-import java.io.InputStreamReader;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
 
 import javax.sql.DataSource;
 
-import com.linecorp.armeria.common.HttpData;
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import com.zaxxer.hikari.HikariDataSource;
+
 import com.linecorp.armeria.common.HttpResponse;
 import com.linecorp.armeria.common.HttpStatus;
 import com.linecorp.armeria.common.MediaType;
+import com.linecorp.armeria.server.ServiceRequestContext;
 import com.linecorp.armeria.server.annotation.Get;
-import com.github.mustachejava.DefaultMustacheFactory;
-import com.github.mustachejava.Mustache;
-import com.github.mustachejava.MustacheFactory;
+
+import hello.helpers.PostgresDbHelper;
+import hello.models.Fortune;
 
 public class PostgresFortunesService {
   private static final String SELECT_QUERY = "SELECT * FROM fortune";
@@ -44,43 +48,51 @@ public class PostgresFortunesService {
   }
 
   @Get("/fortunes")
-  public HttpResponse fortunes() throws SQLException, IOException {
-    List<Fortune> fortunes = getFortunes();
-    fortunes.add(
-        new Fortune(0, "Additional fortune added at request time."));
-    fortunes.sort(Comparator.comparing(Fortune::getMessage));
-
-    return HttpResponse.of(
-        HttpHeadersHelper.getHttpHeader(MediaType.HTML_UTF_8),
-        HttpData.ofUtf8(buildMustacheTemplate(fortunes))
-        );
+  public CompletableFuture<HttpResponse> fortunes() {
+    return getFortunes().thenApply(fortunes -> {
+      fortunes.add(
+          new Fortune(0, "Additional fortune added at request time."));
+      fortunes.sort(Comparator.comparing(Fortune::getMessage));
+
+      return HttpResponse.of(
+              HttpStatus.OK, MediaType.HTML_UTF_8, buildMustacheTemplate(fortunes));
+    });
   }
 
-  private List<Fortune> getFortunes() throws SQLException {
-    List<Fortune> fortunes = new ArrayList<>();
-
-    try (final Connection connection = dataSource.getConnection();
-         final PreparedStatement statement =
-             connection.prepareStatement(SELECT_QUERY);
-         final ResultSet resultSet = statement.executeQuery()) {
-
-      while (resultSet.next()) {
-        fortunes.add(
-            new Fortune(
-                resultSet.getInt(1),
-                resultSet.getString(2)));
-      }
-    }
-    return fortunes;
+  private CompletableFuture<List<Fortune>> getFortunes() {
+    return CompletableFuture.supplyAsync(
+        () -> {
+          List<Fortune> fortunes = new ArrayList<>();
+
+          try (final Connection connection = dataSource.getConnection();
+               final PreparedStatement statement =
+                   connection.prepareStatement(SELECT_QUERY);
+               final ResultSet resultSet = statement.executeQuery()) {
+
+            while (resultSet.next()) {
+              fortunes.add(
+                  new Fortune(
+                      resultSet.getInt(1),
+                      resultSet.getString(2)));
+            }
+          } catch (SQLException e) {
+            throw new IllegalStateException("Database error", e);
+          }
+          return fortunes;
+        }, ServiceRequestContext.current().blockingTaskExecutor()
+    );
   }
 
-  private String buildMustacheTemplate(List<Fortune> fortunes)
-      throws IOException {
+  private byte[] buildMustacheTemplate(List<Fortune> fortunes) {
     Mustache mustache = mustacheFactory.compile("fortunes.mustache");
-    StringWriter stringWriter = new StringWriter();
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
 
-    mustache.execute(stringWriter, fortunes).flush();
+    try (Writer writer = new OutputStreamWriter(bytes, StandardCharsets.UTF_8)) {
+      mustache.execute(writer, fortunes);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
 
-    return stringWriter.toString();
+    return bytes.toByteArray();
   }
 }