Browse Source

Added Vert.x Web Scala. (#4863)

Nikolaj Leischner 6 years ago
parent
commit
a34afcc192

+ 1 - 0
.travis.yml

@@ -112,6 +112,7 @@ env:
     - 'TESTDIR="Scala/akka-http Scala/blaze Scala/cask Scala/colossus Scala/finagle"'
     - 'TESTDIR="Scala/finatra Scala/finch Scala/http4s"'
     - 'TESTDIR="Scala/play2-scala Scala/youi"'
+    - 'TESTDIR="Scala/vertx-web-scala"'
     - "TESTLANG=Scheme"
     - "TESTLANG=Swift"
     - "TESTLANG=TypeScript"

+ 15 - 0
frameworks/Scala/vertx-web-scala/.dockerignore

@@ -0,0 +1,15 @@
+.idea/
+*/*.iml
+target/
+.history
+.cache
+**/*.class
+**/*.log
+
+# sbt specific
+.lib/
+dist/*
+**/target*
+project/target
+project/project/target
+project/plugins/project/

+ 185 - 0
frameworks/Scala/vertx-web-scala/.gitignore

@@ -0,0 +1,185 @@
+### Vert.x ###
+.vertx/
+
+### Eclipse ###
+
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
+
+### Intellij+iml ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# CMake
+cmake-buildTool-debug/
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-buildTool.properties
+fabric.properties
+
+### Intellij+iml Patch ###
+# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
+
+*.iml
+modules.xml
+.idea/misc.xml
+*.ipr
+
+### macOS ###
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+
+# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
+!/.mvn/wrapper/maven-wrapper.jar
+
+### Gradle ###
+.gradle
+/buildTool/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+### NetBeans ###
+nbproject/private/
+buildTool/
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json

+ 2 - 0
frameworks/Scala/vertx-web-scala/.scalafmt.conf

@@ -0,0 +1,2 @@
+version=2.0.0-RC8
+maxColumn = 120

+ 43 - 0
frameworks/Scala/vertx-web-scala/README.md

@@ -0,0 +1,43 @@
+# Vert.x Web Scala Benchmarking Test
+
+This is the Vert.x Web Scala portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+### Test Type Implementation Source Code
+
+* [JSON](src/main/scala/vertx/App.scala)
+* [PLAINTEXT](src/main/scala/vertx/App.scala)
+* [DB](src/main/scala/vertx/App.scala)
+* [QUERY](src/main/scala/vertx/App.scala)
+* [UPDATE](src/main/scala/vertx/App.scala)
+* [FORTUNES](src/main/scala/vertx/App.scala)
+
+## Versions
+
+* [Java OpenJDK 11](https://openjdk.java.net/)
+* [Vert.x 3.7](https://vertx.io/)
+
+## Test URLs
+
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/query?queries=
+
+### UPDATE
+
+http://localhost:8080/update?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes

+ 29 - 0
frameworks/Scala/vertx-web-scala/benchmark_config.json

@@ -0,0 +1,29 @@
+{
+  "framework": "vertx-web-scala",
+  "tests": [
+    {
+      "default": {
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "fortune_url": "/fortunes",
+        "update_url": "/updates?queries=",
+        "plaintext_url": "/plaintext",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "vertx-web",
+        "language": "Scala",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "vertx",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "vertx-web-scala-postgres",
+        "notes": "",
+        "versus": "vertx-web-postgres"
+      }
+    }
+  ]
+}

+ 23 - 0
frameworks/Scala/vertx-web-scala/build.sbt

@@ -0,0 +1,23 @@
+name := "vertx-web-scala"
+
+version := "1"
+
+scalaVersion := "2.12.8"
+
+lazy val root = (project in file(".")).enablePlugins(SbtTwirl)
+
+libraryDependencies += "io.vertx" %% "vertx-lang-scala" % "3.7.1"
+libraryDependencies += "io.vertx" %% "vertx-web-scala" % "3.7.1"
+libraryDependencies += "io.vertx" % "vertx-codegen" % "3.7.1"
+libraryDependencies += "io.netty" % "netty-transport-native-kqueue" % "4.1.36.Final" classifier "osx-x86_64"
+libraryDependencies += "io.netty" % "netty-transport-native-epoll" % "4.1.36.Final" classifier "linux-x86_64"
+libraryDependencies += "io.reactiverse" % "reactive-pg-client" % "0.11.3"
+libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
+libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
+
+mainClass in Compile := Some("vertx.App")
+
+assemblyMergeStrategy in assembly := {
+  case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard
+  case _ => MergeStrategy.first
+}

+ 1 - 0
frameworks/Scala/vertx-web-scala/project/build.properties

@@ -0,0 +1 @@
+sbt.version=1.2.8

+ 3 - 0
frameworks/Scala/vertx-web-scala/project/plugins.sbt

@@ -0,0 +1,3 @@
+addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.4.1")
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.1")
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")

+ 7 - 0
frameworks/Scala/vertx-web-scala/src/main/conf/config.json

@@ -0,0 +1,7 @@
+{
+  "host": "tfb-database",
+  "username": "benchmarkdbuser",
+  "password": "benchmarkdbpass",
+  "database": "hello_world",
+  "maxPoolSize": 64
+}

+ 20 - 0
frameworks/Scala/vertx-web-scala/src/main/resources/logback.xml

@@ -0,0 +1,20 @@
+<configuration>
+
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
+    <appender-ref ref="STDOUT" />
+  </appender>
+
+  <!-- Off these ones as they are annoying, and anyway we manage configuration ourselves -->
+  <logger name="com.avaje.ebean.config.PropertyMapLoader" level="OFF" />
+
+  <root level="INFO">
+    <appender-ref ref="ASYNCSTDOUT" />
+  </root>
+
+</configuration>

+ 294 - 0
frameworks/Scala/vertx-web-scala/src/main/scala/vertx/App.scala

@@ -0,0 +1,294 @@
+package vertx
+
+import java.io.{ByteArrayOutputStream, File, IOException}
+import java.nio.file.Files
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.util.concurrent.ThreadLocalRandom
+
+import com.typesafe.scalalogging.Logger
+import io.reactiverse.pgclient._
+import io.vertx.core.buffer.Buffer
+import io.vertx.core.http.HttpHeaders
+import io.vertx.core.json.{JsonArray, JsonObject}
+import io.vertx.core.{AsyncResult, VertxOptions => JVertxOptions}
+import io.vertx.lang.scala.{ScalaVerticle, VertxExecutionContext}
+import io.vertx.scala.core.http.{HttpServer, HttpServerRequest, HttpServerResponse}
+import io.vertx.scala.core.{VertxOptions, _}
+import io.vertx.scala.ext.web.Router
+import vertx.model.{Fortune, Message, World}
+
+import scala.collection.JavaConverters._
+import scala.util.{Failure, Sorting, Success, Try}
+
+case class Header(name: CharSequence, value: String)
+
+class App extends ScalaVerticle {
+
+  private val HELLO_WORLD = "Hello, world!"
+  private val HELLO_WORLD_BUFFER = Buffer.factory.directBuffer(HELLO_WORLD, "UTF-8")
+  private val SERVER = "vert.x"
+
+  private val contentTypeJson = Header(HttpHeaders.CONTENT_TYPE, "application/json")
+  private val contentTypeHtml = Header(HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
+  private val contentTypePlainText = Header(HttpHeaders.CONTENT_TYPE, "text/plain")
+
+  private var dateString: String = ""
+
+  private var server: HttpServer = _
+  private var client: PgClient = _
+  private var pool: PgPool = _
+
+  private def refreshDateHeader(): Unit = dateString = App.createDateHeader()
+
+  override def start(): Unit = {
+    refreshDateHeader()
+    vertx.setPeriodic(1000, (_: Long) => refreshDateHeader())
+
+    val options = new PgPoolOptions()
+      .setDatabase(config.getString("database"))
+      .setHost(config.getString("host"))
+      .setPort(config.getInteger("port", 5432))
+      .setUser(config.getString("username"))
+      .setPassword(config.getString("password"))
+      .setCachePreparedStatements(true)
+
+    val jVertx = vertx.asJava.asInstanceOf[io.vertx.core.Vertx]
+    client = PgClient.pool(jVertx, new PgPoolOptions(options).setMaxSize(1))
+    pool = PgClient.pool(jVertx, new PgPoolOptions(options).setMaxSize(4))
+
+    val router = Router.router(vertx)
+    router.get("/plaintext").handler(context => handlePlainText(context.request()))
+    router.get("/json").handler(context => handleJson(context.request()))
+    router.get("/db").handler(context => handleDb(context.request()))
+    router.get("/queries").handler(context => handleQueries(context.request()))
+    router.get("/updates").handler(context => handleUpdates(context.request()))
+    router.get("/fortunes").handler(context => handleFortunes(context.request()))
+
+    val port = 8080
+    server = vertx.createHttpServer()
+    server.requestHandler(router.accept).listen(port)
+  }
+
+  override def stop(): Unit = Option(server).foreach(_.close())
+
+  private def responseWithHeaders(response: HttpServerResponse, contentType: Header) =
+    response
+      .putHeader(contentType.name.toString, contentType.value)
+      .putHeader(HttpHeaders.SERVER.toString, SERVER)
+      .putHeader(HttpHeaders.DATE.toString, dateString)
+
+  private def handlePlainText(request: HttpServerRequest): Unit = {
+    responseWithHeaders(request.response, contentTypePlainText).end(HELLO_WORLD_BUFFER)
+  }
+
+  private def handleJson(request: HttpServerRequest): Unit =
+    responseWithHeaders(request.response, contentTypeJson)
+      .end(Message("Hello, World!").toBuffer)
+
+  private def handleDb(request: HttpServerRequest): Unit =
+    client.preparedQuery(
+      "SELECT id, randomnumber from WORLD where id=$1",
+      Tuple.of(App.randomWorld(), Nil: _*),
+      (ar: AsyncResult[PgRowSet]) => {
+        if (ar.succeeded) {
+          val resultSet = ar.result.iterator
+          if (!resultSet.hasNext) {
+            request.response.setStatusCode(404).end()
+          } else {
+            val row = resultSet.next
+            responseWithHeaders(request.response, contentTypeJson)
+              .end(World(row.getInteger(0), row.getInteger(1)).encode())
+          }
+        } else {
+          App.logger.error("Failed to handle request", ar.cause())
+          request.response.setStatusCode(500).end(ar.cause.getMessage)
+        }
+      }
+    )
+
+  private def handleQueries(request: HttpServerRequest): Unit = {
+    val queries = App.getQueries(request)
+    val worlds = new JsonArray
+    var i = 0
+    var failed = false
+    while (i < queries) {
+      client.preparedQuery(
+        "SELECT id, randomnumber from WORLD where id=$1",
+        Tuple.of(App.randomWorld(), Nil: _*),
+        (ar: AsyncResult[PgRowSet]) => {
+          if (!failed) {
+            if (ar.failed) {
+              failed = true
+              request.response.setStatusCode(500).end(ar.cause.getMessage)
+              return
+            }
+            // we need a final reference
+            val row = ar.result.iterator.next
+
+            worlds.add(World(row.getInteger(0), row.getInteger(1)))
+            if (worlds.size == queries)
+              responseWithHeaders(request.response, contentTypeJson)
+                .end(worlds.encode)
+          }
+        }
+      )
+
+      i += 1
+    }
+  }
+
+  private def handleUpdates(request: HttpServerRequest): Unit = {
+    def sendError(err: Throwable): Unit = {
+      App.logger.error("", err)
+      request.response.setStatusCode(500).end(err.getMessage)
+    }
+
+    def handleUpdates(conn: PgConnection, worlds: Array[World]): Unit = {
+      Sorting.quickSort(worlds)
+
+      val batch = worlds.map(world => Tuple.of(world.randomNumber, world.id)).toList.asJava
+      conn.preparedBatch(
+        "UPDATE world SET randomnumber=$1 WHERE id=$2",
+        batch,
+        (ar: AsyncResult[PgRowSet]) => {
+          conn.close()
+          if (ar.failed) {
+            sendError(ar.cause)
+            return
+          }
+
+          responseWithHeaders(request.response, contentTypeJson)
+            .end(new JsonArray(worlds.toList.asJava).toBuffer)
+        }
+      )
+    }
+
+    val queries = App.getQueries(request)
+    val worlds = new Array[World](queries)
+    var failed = false
+    var queryCount = 0
+
+    pool.getConnection((ar1: AsyncResult[PgConnection]) => {
+      if (ar1.failed) {
+        failed = true
+        sendError(ar1.cause)
+        return
+      }
+      val conn = ar1.result
+      var i = 0
+      while (i < worlds.length) {
+        val id = App.randomWorld()
+        val index = i
+        conn.preparedQuery(
+          "SELECT id, randomnumber from WORLD where id=$1",
+          Tuple.of(id, Nil: _*),
+          (ar2: AsyncResult[PgRowSet]) => {
+            if (!failed) {
+              if (ar2.failed) {
+                conn.close()
+                failed = true
+                sendError(ar2.cause)
+                return
+              }
+              worlds(index) = World(ar2.result.iterator.next.getInteger(0), App.randomWorld())
+              queryCount += 1
+              if (queryCount == worlds.length) handleUpdates(conn, worlds)
+            }
+          }
+        )
+
+        i += 1
+      }
+
+    })
+  }
+
+  private def handleFortunes(request: HttpServerRequest): Unit =
+    client.preparedQuery(
+      "SELECT id, message from FORTUNE",
+      (ar: AsyncResult[PgRowSet]) => {
+        val response = request.response
+        if (ar.succeeded) {
+          val resultSet = ar.result.iterator
+          if (!resultSet.hasNext) {
+            response.setStatusCode(404).end("No results")
+            return
+          }
+          val fortunes = (resultSet.asScala
+            .map(row => Fortune(row.getInteger(0), row.getString(1))) ++
+            Seq(Fortune(0, "Additional fortune added at request time."))).toArray
+          Sorting.quickSort(fortunes)
+          responseWithHeaders(request.response, contentTypeHtml)
+            .end(html.fortune(fortunes).body)
+        } else {
+          val err = ar.cause
+          App.logger.error("", err)
+          response.setStatusCode(500).end(err.getMessage)
+        }
+      }
+    )
+
+}
+
+object App {
+  val logger: Logger = Logger[App]
+
+  def main(args: Array[String]): Unit = {
+    val config = new JsonObject(Files.readString(new File(args(0)).toPath))
+    val vertx = Vertx.vertx(VertxOptions().setPreferNativeTransport(true))
+
+    printConfig(vertx)
+
+    vertx.exceptionHandler(_.printStackTrace())
+
+    implicit val executionContext: VertxExecutionContext = VertxExecutionContext(vertx.getOrCreateContext())
+
+    vertx
+      .deployVerticleFuture(
+        ScalaVerticle.nameForVerticle[App],
+        DeploymentOptions().setInstances(JVertxOptions.DEFAULT_EVENT_LOOP_POOL_SIZE).setConfig(config)
+      )
+      .onComplete {
+        case _: Success[String] => logger.info("Server listening on port 8080")
+        case f: Failure[String] => logger.error("Unable to start application", f.exception)
+      }
+  }
+
+  def createDateHeader(): String = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now)
+
+  def randomWorld(): Int = 1 + ThreadLocalRandom.current.nextInt(10000)
+
+  def getQueries(request: HttpServerRequest): Int =
+    request
+      .getParam("queries")
+      .flatMap(param => Try(param.toInt).toOption)
+      .map(number => Math.min(500, Math.max(1, number)))
+      .getOrElse(1)
+
+  private def printConfig(vertx: Vertx): Unit = {
+    val version = Try {
+      def resourceAsStream(resource: String) = classOf[Vertx].getClassLoader.getResourceAsStream(resource)
+
+      val in =
+        Option(resourceAsStream("META-INF/vertx/vertx-version.txt")).getOrElse(resourceAsStream("vertx-version.txt"))
+      val out = new ByteArrayOutputStream
+      val buffer = new Array[Byte](256)
+
+      Iterator
+        .continually(in.read(buffer))
+        .takeWhile(_ != -1)
+        .foreach(read => out.write(buffer, 0, read))
+
+      out.toString
+    }.recover {
+      case e: IOException =>
+        logger.error("Could not read Vert.x version", e)
+        "unknown"
+    }.get
+
+    logger.info("Vert.x: {}", version)
+    logger.info("Event Loop Size: {}", JVertxOptions.DEFAULT_EVENT_LOOP_POOL_SIZE)
+    logger.info("Native transport: {}", vertx.isNativeTransportEnabled)
+  }
+}

+ 15 - 0
frameworks/Scala/vertx-web-scala/src/main/scala/vertx/model/Fortune.scala

@@ -0,0 +1,15 @@
+package vertx.model
+
+import io.vertx.lang.scala.json.JsonObject
+
+object Fortune {
+  private val ID = "id"
+  private val MESSAGE = "message"
+}
+
+case class Fortune(id: Int, message: String) extends JsonObject with Ordered[Fortune] {
+  put(Fortune.ID, id)
+  put(Fortune.MESSAGE, message)
+
+  override def compare(other: Fortune): Int = message.compareTo(other.message)
+}

+ 11 - 0
frameworks/Scala/vertx-web-scala/src/main/scala/vertx/model/Message.scala

@@ -0,0 +1,11 @@
+package vertx.model
+
+import io.vertx.lang.scala.json.JsonObject
+
+object Message {
+  private val MESSAGE = "message"
+}
+
+case class Message(message: String) extends JsonObject {
+  put(Message.MESSAGE, message)
+}

+ 15 - 0
frameworks/Scala/vertx-web-scala/src/main/scala/vertx/model/World.scala

@@ -0,0 +1,15 @@
+package vertx.model
+
+import io.vertx.lang.scala.json.JsonObject
+
+object World {
+  private val ID = "id"
+  private val RANDOM_NUMBER = "randomNumber"
+}
+
+case class World(id: Int, randomNumber: Int) extends JsonObject with Ordered[World] {
+  put(World.ID, id)
+  put(World.RANDOM_NUMBER, randomNumber)
+
+  override def compare(o: World): Int = Integer.compare(id, o.id)
+}

+ 19 - 0
frameworks/Scala/vertx-web-scala/src/main/twirl/vertx/fortune.scala.html

@@ -0,0 +1,19 @@
+@import vertx.model.Fortune
+@(fortunes: Array[Fortune])
+
+@main("Fortunes") {
+  <table>
+    <tr>
+      <th>id</th>
+      <th>message</th>
+    </tr>
+
+    @fortunes.map { case Fortune(id, message) => {
+    <tr>
+      <td>@id</td>
+      <td>@message</td>
+    </tr>
+      }}
+
+  </table>
+}

+ 12 - 0
frameworks/Scala/vertx-web-scala/src/main/twirl/vertx/main.scala.html

@@ -0,0 +1,12 @@
+@(title: String)(content: Html)
+
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <title>@title</title>
+  </head>
+  <body>
+  @content
+  </body>
+</html>

+ 34 - 0
frameworks/Scala/vertx-web-scala/vertx-web-scala.dockerfile

@@ -0,0 +1,34 @@
+FROM openjdk:11-jdk
+
+ARG SBT_VERSION=1.2.8
+
+RUN \
+  curl -L -o sbt-$SBT_VERSION.deb https://dl.bintray.com/sbt/debian/sbt-$SBT_VERSION.deb && \
+  dpkg -i sbt-$SBT_VERSION.deb && \
+  rm sbt-$SBT_VERSION.deb && \
+  apt-get update && \
+  apt-get install sbt && \
+  sbt sbtVersion
+
+WORKDIR /vertx
+COPY src src
+COPY project project
+COPY build.sbt build.sbt
+RUN sbt assembly
+CMD export DBIP=`getent hosts tfb-database | awk '{ print $1 }'` && \
+    sed -i "s|tfb-database|$DBIP|g" /vertx/src/main/conf/config.json && \
+    java \
+      -Xms2G \
+      -Xmx2G \
+      -server \
+      -Dvertx.disableMetrics=true \
+      -Dvertx.disableH2c=true \
+      -Dvertx.disableWebsockets=true \
+      -Dvertx.flashPolicyHandler=false \
+      -Dvertx.threadChecks=false \
+      -Dvertx.disableContextTimings=true \
+      -Dvertx.disableTCCL=true \
+      -Dvertx.disableHttpHeadersValidation=true \
+      -jar \
+      target/scala-2.12/vertx-web-scala-assembly-1.jar \
+      src/main/conf/config.json