Browse Source

Upgrade http4s and make queries in parallel instead of sequentially (#5580)

* Upgrade http4s to Scala 2.13 and the latest version.

Use Circe instead of the previous JSON library

* Execute queries in parallel instead of sequentially

* Upgrade http4s dockerfile to scala 2.13

* Update README.md

* Fix plaintext endpoint
Jordi Olivares Provencio 5 years ago
parent
commit
3307ff40ae

+ 3 - 4
frameworks/Scala/http4s/README.md

@@ -3,7 +3,6 @@
 ## Infrastructure Software Versions
 ## Infrastructure Software Versions
 
 
 The tests were run with:
 The tests were run with:
-
-* [AdoptOpenJDK 11 (`x86_64-alpine-jre-11.0.3_7`)](https://hub.docker.com/r/adoptopenjdk/openjdk11)
-* [http4s 0.20.3](http://http4s.org/)
-* [doobie 0.7.0](https://tpolecat.github.io/doobie/)
+* [OpenJDK](https://hub.docker.com/_/openjdk) 8 for building and the latest alpine for running, as per the [Scala JDK Compatibility page](https://docs.scala-lang.org/overviews/jdk-compatibility/overview.html)
+* [http4s 0.21.3](http://http4s.org/)
+* [doobie 0.8.8](https://tpolecat.github.io/doobie/)

+ 9 - 11
frameworks/Scala/http4s/build.sbt

@@ -2,35 +2,33 @@ name := "http4s"
 
 
 version := "1.0"
 version := "1.0"
 
 
-scalaVersion := "2.12.8"
+scalaVersion := "2.13.1"
 
 
 scalacOptions ++= Seq(
 scalacOptions ++= Seq(
   "-deprecation",
   "-deprecation",
-  "-encoding", "UTF-8",
+  "-encoding",
+  "UTF-8",
   "-feature",
   "-feature",
   "-unchecked",
   "-unchecked",
   "-language:reflectiveCalls",
   "-language:reflectiveCalls",
-  "-Yno-adapted-args",
-  "-Ypartial-unification",
   "-Ywarn-numeric-widen",
   "-Ywarn-numeric-widen",
-  "-Xfuture",
   "-Xlint"
   "-Xlint"
 )
 )
 
 
 enablePlugins(SbtTwirl)
 enablePlugins(SbtTwirl)
 
 
-TwirlKeys.templateImports += "http4s.techempower.benchmark._"
-
-val http4sVersion = "0.20.3"
-val doobieVersion = "0.7.0"
+val http4sVersion = "0.21.3"
+val doobieVersion = "0.8.8"
 
 
 libraryDependencies ++= Seq(
 libraryDependencies ++= Seq(
   "org.http4s" %% "http4s-blaze-server" % http4sVersion,
   "org.http4s" %% "http4s-blaze-server" % http4sVersion,
   "org.http4s" %% "http4s-dsl" % http4sVersion,
   "org.http4s" %% "http4s-dsl" % http4sVersion,
   "org.http4s" %% "http4s-twirl" % http4sVersion,
   "org.http4s" %% "http4s-twirl" % http4sVersion,
-  "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "0.51.3",
+  "org.http4s" %% "http4s-circe" % http4sVersion,
+  // Optional for auto-derivation of JSON codecs
+  "io.circe" %% "circe-generic" % "0.13.0",
   "org.tpolecat" %% "doobie-core" % doobieVersion,
   "org.tpolecat" %% "doobie-core" % doobieVersion,
   "org.tpolecat" %% "doobie-hikari" % doobieVersion,
   "org.tpolecat" %% "doobie-hikari" % doobieVersion,
-  "org.postgresql" % "postgresql" % "42.2.6",
+  "org.tpolecat" %% "doobie-postgres" % doobieVersion,
   "ch.qos.logback" % "logback-classic" % "1.2.3"
   "ch.qos.logback" % "logback-classic" % "1.2.3"
 )
 )

+ 5 - 3
frameworks/Scala/http4s/http4s.dockerfile

@@ -1,15 +1,17 @@
-FROM adoptopenjdk/openjdk11:x86_64-alpine-jre-11.0.3_7
-RUN apk add bash
+FROM openjdk:8 AS builder
 WORKDIR /http4s
 WORKDIR /http4s
 COPY project project
 COPY project project
 COPY src src
 COPY src src
 COPY build.sbt build.sbt
 COPY build.sbt build.sbt
 COPY sbt sbt
 COPY sbt sbt
 RUN ./sbt assembly -batch && \
 RUN ./sbt assembly -batch && \
-    mv target/scala-2.12/http4s-assembly-1.0.jar . && \
+    mv target/scala-2.13/http4s-assembly-1.0.jar . && \
     rm -Rf target && \
     rm -Rf target && \
     rm -Rf project/target && \
     rm -Rf project/target && \
     rm -Rf ~/.sbt && \
     rm -Rf ~/.sbt && \
     rm -Rf ~/.ivy2 && \
     rm -Rf ~/.ivy2 && \
     rm -Rf /var/cache
     rm -Rf /var/cache
+FROM openjdk:alpine
+WORKDIR /http4s
+COPY --from=builder /http4s/http4s-assembly-1.0.jar /http4s/http4s-assembly-1.0.jar
 CMD ["java", "-server", "-Xms2g", "-Xmx2g", "-XX:NewSize=1g", "-XX:MaxNewSize=1g", "-XX:InitialCodeCacheSize=256m", "-XX:ReservedCodeCacheSize=256m", "-XX:+UseParallelGC", "-XX:+UseNUMA", "-XX:-UseBiasedLocking", "-XX:+AlwaysPreTouch", "-jar", "http4s-assembly-1.0.jar", "tfb-database"]
 CMD ["java", "-server", "-Xms2g", "-Xmx2g", "-XX:NewSize=1g", "-XX:MaxNewSize=1g", "-XX:InitialCodeCacheSize=256m", "-XX:ReservedCodeCacheSize=256m", "-XX:+UseParallelGC", "-XX:+UseNUMA", "-XX:-UseBiasedLocking", "-XX:+AlwaysPreTouch", "-jar", "http4s-assembly-1.0.jar", "tfb-database"]

+ 1 - 1
frameworks/Scala/http4s/project/build.properties

@@ -1 +1 @@
-sbt.version=1.2.8
+sbt.version=1.3.9

+ 1 - 1
frameworks/Scala/http4s/project/plugins.sbt

@@ -1,3 +1,3 @@
 addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
 addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
 
 
-addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.4.2")
+addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.0")

+ 53 - 68
frameworks/Scala/http4s/src/main/scala/WebServer.scala → frameworks/Scala/http4s/src/main/scala/http4s/techempower/benchmark/WebServer.scala

@@ -3,16 +3,18 @@ package http4s.techempower.benchmark
 import java.util.concurrent.ThreadLocalRandom
 import java.util.concurrent.ThreadLocalRandom
 
 
 import cats.effect._
 import cats.effect._
-import cats.implicits._
-import com.github.plokhotnyuk.jsoniter_scala.core._
-import com.github.plokhotnyuk.jsoniter_scala.macros._
+import cats.instances.list._
+import cats.syntax.parallel._
+import cats.syntax.traverse._
+import io.circe.generic.auto._
+import io.circe.syntax._
 import doobie._
 import doobie._
-import doobie.hikari.HikariTransactor
 import doobie.implicits._
 import doobie.implicits._
+import doobie.hikari.HikariTransactor
 import doobie.util.ExecutionContexts
 import doobie.util.ExecutionContexts
 import org.http4s._
 import org.http4s._
 import org.http4s.dsl._
 import org.http4s.dsl._
-import org.http4s.headers.`Content-Type`
+import org.http4s.circe._
 import org.http4s.implicits._
 import org.http4s.implicits._
 import org.http4s.server.Router
 import org.http4s.server.Router
 import org.http4s.server.blaze.BlazeServerBuilder
 import org.http4s.server.blaze.BlazeServerBuilder
@@ -24,46 +26,36 @@ case class Fortune(id: Int, message: String)
 
 
 // Extract queries parameter (with default and min/maxed)
 // Extract queries parameter (with default and min/maxed)
 object Queries {
 object Queries {
-  def unapply(params: Map[String,Seq[String]]): Option[Int] =
-    Some(params.get("queries").getOrElse(Nil).headOption match {
+  def unapply(params: Map[String, Seq[String]]): Option[Int] =
+    Some(params.getOrElse("queries", Nil).headOption match {
       case None => 1
       case None => 1
-      case Some(x) => Math.max(1, Math.min(500, scala.util.Try(x.toInt).getOrElse(1)))
+      case Some(x) =>
+        Math.max(1, Math.min(500, scala.util.Try(x.toInt).getOrElse(1)))
     })
     })
 }
 }
 
 
-object JsonSupport {
-  implicit val messageCodec: JsonValueCodec[Message] = JsonCodecMaker.make[Message](CodecMakerConfig())
-  implicit val worldCodec: JsonValueCodec[World] = JsonCodecMaker.make[World](CodecMakerConfig())
-  implicit val worldListCodec: JsonValueCodec[List[World]] = JsonCodecMaker.make[List[World]](CodecMakerConfig())
-  implicit val fortuneCodec: JsonValueCodec[Fortune] = JsonCodecMaker.make[Fortune](CodecMakerConfig())
-
-  implicit def jsonEncoder[T: JsonValueCodec]: EntityEncoder[IO, T] =
-    EntityEncoder
-      .byteArrayEncoder[IO]
-      .contramap((data: T) => writeToArray(data))
-      .withContentType(`Content-Type`(MediaType.application.json, Some(Charset.`UTF-8`)))
-}
-
 object WebServer extends IOApp with Http4sDsl[IO] {
 object WebServer extends IOApp with Http4sDsl[IO] {
-  import JsonSupport._
-
-  def openDatabase(host: String, poolSize: Int): Resource[IO, HikariTransactor[IO]] =
+  def openDatabase(host: String,
+                   poolSize: Int): Resource[IO, HikariTransactor[IO]] =
     for {
     for {
       ce <- ExecutionContexts.fixedThreadPool[IO](32) // our connect EC
       ce <- ExecutionContexts.fixedThreadPool[IO](32) // our connect EC
-      te <- ExecutionContexts.cachedThreadPool[IO]    // our transaction TE
+      be <- Blocker[IO] // our blocking EC
       xa <- HikariTransactor.newHikariTransactor[IO](
       xa <- HikariTransactor.newHikariTransactor[IO](
         "org.postgresql.Driver",
         "org.postgresql.Driver",
         s"jdbc:postgresql://$host/hello_world",
         s"jdbc:postgresql://$host/hello_world",
         "benchmarkdbuser",
         "benchmarkdbuser",
         "benchmarkdbpass",
         "benchmarkdbpass",
         ce,
         ce,
-        te
+        be
       )
       )
-      _  <- Resource.liftF(
-        xa.configure(ds => IO {
-         ds.setMaximumPoolSize(poolSize)
-         ds.setMinimumIdle(poolSize)
-        })
+      _ <- Resource.liftF(
+        xa.configure(
+          ds =>
+            IO {
+              ds.setMaximumPoolSize(poolSize)
+              ds.setMinimumIdle(poolSize)
+          }
+        )
       )
       )
     } yield xa
     } yield xa
 
 
@@ -72,9 +64,7 @@ object WebServer extends IOApp with Http4sDsl[IO] {
 
 
   // Update the randomNumber field with a random number
   // Update the randomNumber field with a random number
   def updateRandomNumber(world: World): IO[World] =
   def updateRandomNumber(world: World): IO[World] =
-    randomWorldId.map(id =>
-      world.copy(randomNumber = id)
-    )
+    randomWorldId.map(id => world.copy(randomNumber = id))
 
 
   // Select a World object from the database by ID
   // Select a World object from the database by ID
   def selectWorld(xa: Transactor[IO], id: Int): IO[World] =
   def selectWorld(xa: Transactor[IO], id: Int): IO[World] =
@@ -91,7 +81,7 @@ object WebServer extends IOApp with Http4sDsl[IO] {
 
 
   // Select a specified number of random World objects from the database
   // Select a specified number of random World objects from the database
   def getWorlds(xa: Transactor[IO], numQueries: Int): IO[List[World]] =
   def getWorlds(xa: Transactor[IO], numQueries: Int): IO[List[World]] =
-    (0 until numQueries).toList.traverse(_ => selectRandomWorld(xa))
+    (0 until numQueries).toList.parTraverse(_ => selectRandomWorld(xa))
 
 
   // Update the randomNumber field with a new random number, for a list of World objects
   // Update the randomNumber field with a new random number, for a list of World objects
   def getNewWorlds(worlds: List[World]): IO[List[World]] =
   def getNewWorlds(worlds: List[World]): IO[List[World]] =
@@ -102,7 +92,8 @@ object WebServer extends IOApp with Http4sDsl[IO] {
   def updateWorlds(xa: Transactor[IO], newWorlds: List[World]): IO[Int] = {
   def updateWorlds(xa: Transactor[IO], newWorlds: List[World]): IO[Int] = {
     val sql = "update World set randomNumber = ? where id = ?"
     val sql = "update World set randomNumber = ? where id = ?"
     // Reason for sorting: https://github.com/TechEmpower/FrameworkBenchmarks/pull/4214#issuecomment-489358881
     // Reason for sorting: https://github.com/TechEmpower/FrameworkBenchmarks/pull/4214#issuecomment-489358881
-    val update = Update[(Int, Int)](sql).updateMany(newWorlds.sortBy(_.id).map(w => (w.randomNumber, w.id)))
+    val update = Update[(Int, Int)](sql)
+      .updateMany(newWorlds.sortBy(_.id).map(w => (w.randomNumber, w.id)))
     update.transact(xa)
     update.transact(xa)
   }
   }
 
 
@@ -126,38 +117,32 @@ object WebServer extends IOApp with Http4sDsl[IO] {
 
 
   // HTTP service definition
   // HTTP service definition
   def service(xa: Transactor[IO]) =
   def service(xa: Transactor[IO]) =
-    addServerHeader(
-      HttpRoutes.of[IO] {
-        case GET -> Root / "plaintext" =>
-          Ok("Hello, World!")
-
-        case GET -> Root / "json" =>
-          Ok(Message("Hello, World!"))
-
-        case GET -> Root / "db" =>
-          Ok(selectRandomWorld(xa))
-
-        case GET -> Root / "queries" :? Queries(numQueries) =>
-          Ok(getWorlds(xa, numQueries))
-
-        case GET -> Root / "fortunes" =>
-          Ok(
-            for {
-              oldFortunes <- getFortunes(xa)
-              newFortunes = getSortedFortunes(oldFortunes)
-            } yield html.index(newFortunes)
-          )
-
-        case GET -> Root / "updates" :? Queries(numQueries) =>
-          Ok(
-            for {
-              worlds <- getWorlds(xa, numQueries)
-              newWorlds <- getNewWorlds(worlds)
-              _ <- updateWorlds(xa, newWorlds)
-            } yield newWorlds
-          )
-      }
-    )
+    addServerHeader(HttpRoutes.of[IO] {
+      case GET -> Root / "plaintext" =>
+        Ok("Hello, World!")
+
+      case GET -> Root / "json" =>
+        Ok(Message("Hello, World!").asJson)
+
+      case GET -> Root / "db" =>
+        Ok(selectRandomWorld(xa).map(_.asJson))
+
+      case GET -> Root / "queries" :? Queries(numQueries) =>
+        Ok(getWorlds(xa, numQueries).map(_.asJson))
+
+      case GET -> Root / "fortunes" =>
+        Ok(for {
+          oldFortunes <- getFortunes(xa)
+          newFortunes = getSortedFortunes(oldFortunes)
+        } yield html.index(newFortunes))
+
+      case GET -> Root / "updates" :? Queries(numQueries) =>
+        Ok(for {
+          worlds <- getWorlds(xa, numQueries)
+          newWorlds <- getNewWorlds(worlds)
+          _ <- updateWorlds(xa, newWorlds)
+        } yield newWorlds.asJson)
+    })
 
 
   // Given a fully constructed HttpService, start the server and wait for completion
   // Given a fully constructed HttpService, start the server and wait for completion
   def startServer(service: HttpRoutes[IO]) =
   def startServer(service: HttpRoutes[IO]) =

+ 1 - 0
frameworks/Scala/http4s/src/main/twirl/index.scala.html

@@ -1,3 +1,4 @@
+@import http4s.techempower.benchmark.Fortune
 @(fortunes: Seq[Fortune])
 @(fortunes: Seq[Fortune])
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html>
 <html>