Browse Source

Implement caching test and refactor whole Hexagon benchmark (#6668)

* Implement caching test and refactor whole Hexagon benchmark

* Ignore Gradle generated artifacts

* Fix servlet server issue

* Fix servlet server issue

* Fix servlet server issue

* Fix servlet server issue

* Clean up

* Clean up

* Fix fortunes

* Fix Resin server

* Fix Resin server

* Fix Resin server

* Delete tests
Juanjo Aguililla 4 years ago
parent
commit
977a78d612
25 changed files with 383 additions and 456 deletions
  1. 4 0
      .gitignore
  2. 4 0
      frameworks/Kotlin/hexagon/benchmark_config.json
  3. 19 6
      frameworks/Kotlin/hexagon/build.gradle
  4. 4 0
      frameworks/Kotlin/hexagon/config.toml
  5. 0 9
      frameworks/Kotlin/hexagon/gradle.properties
  6. BIN
      frameworks/Kotlin/hexagon/gradle/wrapper/gradle-wrapper.jar
  7. 0 5
      frameworks/Kotlin/hexagon/gradle/wrapper/gradle-wrapper.properties
  8. 1 2
      frameworks/Kotlin/hexagon/hexagon-jetty-postgresql.dockerfile
  9. 1 4
      frameworks/Kotlin/hexagon/hexagon-resin-mongodb.dockerfile
  10. 1 4
      frameworks/Kotlin/hexagon/hexagon-resin-postgresql.dockerfile
  11. 1 2
      frameworks/Kotlin/hexagon/hexagon.dockerfile
  12. 0 2
      frameworks/Kotlin/hexagon/settings.gradle
  13. 23 91
      frameworks/Kotlin/hexagon/src/main/kotlin/Benchmark.kt
  14. 0 141
      frameworks/Kotlin/hexagon/src/main/kotlin/BenchmarkStorage.kt
  15. 80 0
      frameworks/Kotlin/hexagon/src/main/kotlin/Controller.kt
  16. 6 0
      frameworks/Kotlin/hexagon/src/main/kotlin/Model.kt
  17. 28 0
      frameworks/Kotlin/hexagon/src/main/kotlin/Settings.kt
  18. 19 0
      frameworks/Kotlin/hexagon/src/main/kotlin/WebListenerServer.kt
  19. 57 0
      frameworks/Kotlin/hexagon/src/main/kotlin/store/BenchmarkMongoDbStore.kt
  20. 101 0
      frameworks/Kotlin/hexagon/src/main/kotlin/store/BenchmarkSqlStore.kt
  21. 33 0
      frameworks/Kotlin/hexagon/src/main/kotlin/store/BenchmarkStore.kt
  22. 0 14
      frameworks/Kotlin/hexagon/src/main/resources/application.yml
  23. 1 1
      frameworks/Kotlin/hexagon/src/main/resources/fortunes.pebble.html
  24. 0 172
      frameworks/Kotlin/hexagon/src/test/kotlin/BenchmarkTest.kt
  25. 0 3
      frameworks/Kotlin/hexagon/src/test/resources/application_test.yml

+ 4 - 0
.gitignore

@@ -24,6 +24,10 @@ mods/
 *.versionsBackup
 bin/
 
+# gradle
+build/
+.gradle/
+
 # common junk
 *.log
 *.swp

+ 4 - 0
frameworks/Kotlin/hexagon/benchmark_config.json

@@ -8,6 +8,7 @@
                 "query_url": "/mongodb/query?queries=",
                 "fortune_url": "/mongodb/pebble/fortunes",
                 "update_url": "/mongodb/update?queries=",
+                "cached_query_url": "/mongodb/cached?count=",
                 "plaintext_url": "/plaintext",
                 "port": 9090,
                 "approach": "Realistic",
@@ -30,6 +31,7 @@
                 "query_url": "/mongodb/query?queries=",
                 "fortune_url": "/mongodb/pebble/fortunes",
                 "update_url": "/mongodb/update?queries=",
+                "cached_query_url": "/mongodb/cached?count=",
                 "plaintext_url": "/plaintext",
                 "port": 8080,
                 "approach": "Realistic",
@@ -52,6 +54,7 @@
                 "query_url": "/postgresql/query?queries=",
                 "fortune_url": "/postgresql/pebble/fortunes",
                 "update_url": "/postgresql/update?queries=",
+                "cached_query_url": "/postgresql/cached?count=",
                 "plaintext_url": "/plaintext",
                 "port": 9090,
                 "approach": "Realistic",
@@ -74,6 +77,7 @@
                 "query_url": "/postgresql/query?queries=",
                 "fortune_url": "/postgresql/pebble/fortunes",
                 "update_url": "/postgresql/update?queries=",
+                "cached_query_url": "/postgresql/cached?count=",
                 "plaintext_url": "/plaintext",
                 "port": 8080,
                 "approach": "Realistic",

+ 19 - 6
frameworks/Kotlin/hexagon/build.gradle

@@ -1,6 +1,18 @@
 
 plugins {
-    id "org.jetbrains.kotlin.jvm" version "1.3.72"
+    id "org.jetbrains.kotlin.jvm" version "1.5.10"
+}
+
+ext {
+    gradleScripts = "https://raw.githubusercontent.com/hexagonkt/hexagon/1.3.19/gradle"
+
+    hexagonVersion = "1.3.19"
+    hikariVersion = "4.0.3"
+    jettyVersion = "10.0.5"
+    postgresqlVersion = "42.2.21"
+    testcontainersVersion = "1.15.3"
+    cache2kVersion = "2.0.0.Final"
+    jacksonBlackbirdVersion = "2.12.3"
 }
 
 apply(from: "$gradleScripts/kotlin.gradle")
@@ -11,7 +23,7 @@ apply(plugin: "war")
 defaultTasks("installDist")
 
 application {
-    mainClassName = "com.hexagonkt.BenchmarkKt"
+    mainClass.set("com.hexagonkt.BenchmarkKt")
 }
 
 war {
@@ -24,16 +36,17 @@ dependencies {
     implementation("com.hexagonkt:store_mongodb:$hexagonVersion")
     implementation("com.hexagonkt:http_server_jetty:$hexagonVersion")
     implementation("com.hexagonkt:templates_pebble:$hexagonVersion")
+    implementation("com.hexagonkt:logging_slf4j_jul:$hexagonVersion")
+    implementation("com.hexagonkt:serialization_json:$hexagonVersion")
 
+    implementation("com.fasterxml.jackson.module:jackson-module-blackbird:$jacksonBlackbirdVersion")
+    implementation("org.cache2k:cache2k-core:$cache2kVersion")
     implementation("com.zaxxer:HikariCP:$hikariVersion")
     implementation("org.postgresql:postgresql:$postgresqlVersion")
 
-    implementation("org.slf4j:slf4j-jdk14:1.7.30")
-    runtimeOnly("org.slf4j:jcl-over-slf4j:1.7.30")
-    runtimeOnly("org.slf4j:log4j-over-slf4j:1.7.30")
-
     // providedCompile excludes the dependency only in the WAR, not in the distribution
     providedCompile("org.eclipse.jetty:jetty-webapp:$jettyVersion") { exclude module: "slf4j-api" }
 
     testImplementation("com.hexagonkt:http_client_ahc:$hexagonVersion")
+    testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
 }

+ 4 - 0
frameworks/Kotlin/hexagon/config.toml

@@ -8,6 +8,7 @@ urls.db = "/mongodb/db"
 urls.query = "/mongodb/query?queries="
 urls.update = "/mongodb/update?queries="
 urls.fortune = "/mongodb/pebble/fortunes"
+urls.cached_query = "/mongodb/cached?count="
 approach = "Realistic"
 classification = "Micro"
 database = "mongodb"
@@ -25,6 +26,7 @@ urls.db = "/postgresql/db"
 urls.query = "/postgresql/query?queries="
 urls.update = "/postgresql/update?queries="
 urls.fortune = "/postgresql/pebble/fortunes"
+urls.cached_query = "/postgresql/cached?count="
 approach = "Realistic"
 classification = "Micro"
 database = "postgres"
@@ -42,6 +44,7 @@ urls.db = "/postgresql/db"
 urls.query = "/postgresql/query?queries="
 urls.update = "/postgresql/update?queries="
 urls.fortune = "/postgresql/pebble/fortunes"
+urls.cached_query = "/postgresql/cached?count="
 approach = "Realistic"
 classification = "Micro"
 database = "postgres"
@@ -59,6 +62,7 @@ urls.db = "/mongodb/db"
 urls.query = "/mongodb/query?queries="
 urls.update = "/mongodb/update?queries="
 urls.fortune = "/mongodb/pebble/fortunes"
+urls.cached_query = "/mongodb/cached?count="
 approach = "Realistic"
 classification = "Micro"
 database = "mongodb"

+ 0 - 9
frameworks/Kotlin/hexagon/gradle.properties

@@ -1,9 +0,0 @@
-description=Hexagon web framework's benchmark
-gradleScripts=https://raw.githubusercontent.com/hexagonkt/hexagon/1.2.24/gradle
-hexagonVersion=1.2.24
-hikariVersion=3.4.5
-jettyVersion=9.4.31.v20200723
-logbackVersion=1.2.3
-name=hexagon
-postgresqlVersion=42.2.12
-testngVersion=6.14.3

BIN
frameworks/Kotlin/hexagon/gradle/wrapper/gradle-wrapper.jar


+ 0 - 5
frameworks/Kotlin/hexagon/gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https://services.gradle.org/distributions/gradle-6.6-all.zip
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists

+ 1 - 2
frameworks/Kotlin/hexagon/hexagon-jetty-postgresql.dockerfile

@@ -1,13 +1,12 @@
 #
 # BUILD
 #
-FROM gradle:6.6-jdk11 AS gradle_build
+FROM gradle:7.1-jdk11 AS gradle_build
 USER root
 WORKDIR /hexagon
 
 COPY src src
 COPY build.gradle build.gradle
-COPY gradle.properties gradle.properties
 RUN gradle --quiet --exclude-task test
 
 #

+ 1 - 4
frameworks/Kotlin/hexagon/hexagon-resin-mongodb.dockerfile

@@ -1,13 +1,12 @@
 #
 # BUILD
 #
-FROM gradle:6.6-jdk11 AS gradle_build
+FROM gradle:7.1-jdk11 AS gradle_build
 USER root
 WORKDIR /hexagon
 
 COPY src src
 COPY build.gradle build.gradle
-COPY gradle.properties gradle.properties
 RUN gradle --quiet --exclude-task test
 
 #
@@ -22,7 +21,5 @@ WORKDIR /resin
 RUN curl -sL $RESIN | tar xz --strip-components=1
 RUN rm -rf webapps/*
 COPY --from=gradle_build /hexagon/build/libs/ROOT.war webapps/ROOT.war
-
 EXPOSE 9090
-
 CMD ["java", "-jar", "lib/resin.jar", "console"]

+ 1 - 4
frameworks/Kotlin/hexagon/hexagon-resin-postgresql.dockerfile

@@ -1,13 +1,12 @@
 #
 # BUILD
 #
-FROM gradle:6.6-jdk11 AS gradle_build
+FROM gradle:7.1-jdk11 AS gradle_build
 USER root
 WORKDIR /hexagon
 
 COPY src src
 COPY build.gradle build.gradle
-COPY gradle.properties gradle.properties
 RUN gradle --quiet --exclude-task test
 
 #
@@ -22,7 +21,5 @@ WORKDIR /resin
 RUN curl -sL $RESIN | tar xz --strip-components=1
 RUN rm -rf webapps/*
 COPY --from=gradle_build /hexagon/build/libs/ROOT.war webapps/ROOT.war
-
 EXPOSE 8080
-
 CMD ["java", "-jar", "lib/resin.jar", "console"]

+ 1 - 2
frameworks/Kotlin/hexagon/hexagon.dockerfile

@@ -1,13 +1,12 @@
 #
 # BUILD
 #
-FROM gradle:6.6-jdk11 AS gradle_build
+FROM gradle:7.1-jdk11 AS gradle_build
 USER root
 WORKDIR /hexagon
 
 COPY src src
 COPY build.gradle build.gradle
-COPY gradle.properties gradle.properties
 RUN gradle --quiet --exclude-task test
 
 #

+ 0 - 2
frameworks/Kotlin/hexagon/settings.gradle

@@ -1,2 +0,0 @@
-
-rootProject.name = name

+ 23 - 91
frameworks/Kotlin/hexagon/src/main/kotlin/Benchmark.kt

@@ -1,28 +1,17 @@
 package com.hexagonkt
 
-import com.hexagonkt.helpers.Jvm.systemSetting
-import com.hexagonkt.serialization.Json
-import com.hexagonkt.serialization.convertToMap
-import com.hexagonkt.http.server.*
+import com.fasterxml.jackson.module.blackbird.BlackbirdModule
+import com.hexagonkt.http.server.Server
+import com.hexagonkt.http.server.ServerPort
+import com.hexagonkt.http.server.ServerSettings
 import com.hexagonkt.http.server.jetty.JettyServletAdapter
-import com.hexagonkt.http.server.servlet.ServletServer
-import com.hexagonkt.settings.SettingsManager
-import com.hexagonkt.templates.TemplateManager.render
+import com.hexagonkt.serialization.*
+import com.hexagonkt.store.BenchmarkMongoDbStore
+import com.hexagonkt.store.BenchmarkSqlStore
+import com.hexagonkt.store.BenchmarkStore
 import com.hexagonkt.templates.TemplatePort
 import com.hexagonkt.templates.pebble.PebbleAdapter
-
-import java.util.Locale
-
-import javax.servlet.annotation.WebListener
-
-// DATA CLASSES
-internal data class Message(val message: String)
-internal data class Fortune(val _id: Int, val message: String)
-internal data class World(val _id: Int, val id: Int, val randomNumber: Int)
-
-// CONSTANTS
-private const val TEXT_MESSAGE: String = "Hello, World!"
-private const val QUERIES_PARAM: String = "queries"
+import java.net.InetAddress
 
 internal val benchmarkStores: Map<String, BenchmarkStore> by lazy {
     mapOf(
@@ -35,82 +24,25 @@ internal val benchmarkTemplateEngines: Map<String, TemplatePort> by lazy {
     mapOf("pebble" to PebbleAdapter)
 }
 
-internal val engine by lazy { createEngine() }
-
-private val defaultLocale = Locale.getDefault()
-
-private val router: Router by lazy {
-    Router {
-        before {
-            response.headers["Server"] = "Servlet/3.1"
-            response.headers["Transfer-Encoding"] = "chunked"
-        }
-
-        get("/plaintext") { ok(TEXT_MESSAGE, "text/plain") }
-        get("/json") { ok(Message(TEXT_MESSAGE), Json) }
-
-        benchmarkStores.forEach { (storeEngine, store) ->
-            benchmarkTemplateEngines.forEach { templateKind ->
-                val path = "/$storeEngine/${templateKind.key}/fortunes"
-
-                get(path) { listFortunes(store, templateKind.key, templateKind.value) }
-            }
-
-            get("/$storeEngine/db") { dbQuery(store) }
-            get("/$storeEngine/query") { getWorlds(store) }
-            get("/$storeEngine/update") { updateWorlds(store) }
-        }
-    }
-}
-
-internal val benchmarkServer: Server by lazy { Server(engine, router, SettingsManager.settings) }
-
-// UTILITIES
-internal fun createEngine(): ServerPort = when (systemSetting("WEBENGINE", "jetty")) {
-    "jetty" -> JettyServletAdapter()
-    else -> error("Unsupported server engine")
+internal val benchmarkEngines: Map<String, ServerPort> by lazy {
+    mapOf("jetty" to JettyServletAdapter())
 }
 
-private fun returnWorlds(worldsList: List<World>): List<Map<Any?, Any?>> =
-    worldsList.map { it.convertToMap() - "_id" }
-
-private fun Call.getWorldsCount(): Int =
-    queryParametersValues[QUERIES_PARAM]?.firstOrNull()?.toIntOrNull().let {
-        when {
-            it == null -> 1
-            it < 1 -> 1
-            it > 500 -> 500
-            else -> it
-        }
-    }
-
-// HANDLERS
-private fun Call.listFortunes(
-    store: BenchmarkStore, templateKind: String, templateAdapter: TemplatePort) {
-
-    val fortunes = store.findAllFortunes() + Fortune(0, "Additional fortune added at request time.")
-    val sortedFortunes = fortunes.sortedBy { it.message }
-    val context = mapOf("fortunes" to sortedFortunes)
-
-    response.contentType = "text/html;charset=utf-8"
-    ok(render(templateAdapter, "fortunes.$templateKind.html", defaultLocale, context))
-}
-
-private fun Call.dbQuery(store: BenchmarkStore) {
-    ok(returnWorlds(store.findWorlds(1)).first(), Json)
-}
-
-private fun Call.getWorlds(store: BenchmarkStore) {
-    ok(returnWorlds(store.findWorlds(getWorldsCount())), Json)
-}
+internal val benchmarkServer: Server by lazy {
+    val settings = Settings()
+    val engine = benchmarkEngines[settings.webEngine] ?: error("Unsupported server engine")
+    val serverSettings = ServerSettings(
+        bindAddress = InetAddress.getByName(settings.bindAddress),
+        bindPort = settings.bindPort
+    )
 
-private fun Call.updateWorlds(store: BenchmarkStore) {
-    ok(returnWorlds(store.replaceWorlds(getWorldsCount())), Json)
+    Server(engine, Controller(settings).router, serverSettings)
 }
 
-// SERVERS
-@WebListener class Web : ServletServer(router)
-
 fun main() {
+    Json.mapper.registerModule(BlackbirdModule())
+    SerializationManager.mapper = JacksonMapper
+    SerializationManager.formats = linkedSetOf(Json)
+
     benchmarkServer.start()
 }

+ 0 - 141
frameworks/Kotlin/hexagon/src/main/kotlin/BenchmarkStorage.kt

@@ -1,141 +0,0 @@
-package com.hexagonkt
-
-import com.hexagonkt.helpers.fail
-import com.hexagonkt.helpers.Jvm.systemSetting
-import com.hexagonkt.settings.SettingsManager.defaultSetting
-import com.hexagonkt.store.mongodb.MongoDbStore
-
-import com.zaxxer.hikari.HikariConfig
-import com.zaxxer.hikari.HikariDataSource
-
-import java.sql.Connection
-import java.util.concurrent.ThreadLocalRandom
-
-internal const val WORLD_ROWS: Int = 10_000
-
-private val worldName: String = defaultSetting("worldCollection", "world")
-private val fortuneName: String = defaultSetting("fortuneCollection", "fortune")
-private val databaseName: String = defaultSetting("database", "hello_world")
-
-internal fun randomWorld(): Int = ThreadLocalRandom.current().nextInt(WORLD_ROWS) + 1
-
-internal interface BenchmarkStore {
-    fun findAllFortunes(): List<Fortune>
-    fun findWorlds(count: Int): List<World>
-    fun replaceWorlds(count: Int): List<World>
-    fun close()
-}
-
-internal class BenchmarkMongoDbStore(engine: String) : BenchmarkStore {
-
-    private val dbHost: String = systemSetting("${engine.toUpperCase()}_DB_HOST", "localhost")
-
-    private val dbUrl: String = "mongodb://$dbHost/$databaseName"
-
-    private val worldRepository by lazy {
-        MongoDbStore(World::class, World::_id, dbUrl, worldName)
-    }
-
-    private val fortuneRepository by lazy {
-        MongoDbStore(Fortune::class, Fortune::_id, dbUrl, fortuneName)
-    }
-
-    override fun findAllFortunes(): List<Fortune> = fortuneRepository.findAll()
-
-    override fun findWorlds(count: Int): List<World> =
-        (1..count).mapNotNull { worldRepository.findOne(randomWorld()) }
-
-    override fun replaceWorlds(count: Int): List<World> = (1..count)
-        .map {
-            val world = worldRepository.findOne(randomWorld()) ?: fail
-            val worldCopy = world.copy(randomNumber = randomWorld())
-            worldRepository.replaceOne(worldCopy)
-            worldCopy
-        }
-
-    override fun close() {
-        /* Not needed */
-    }
-}
-
-internal class BenchmarkSqlStore(engine: String) : BenchmarkStore {
-    companion object {
-        private const val SELECT_WORLD = "select * from world where id = ?"
-        private const val UPDATE_WORLD = "update world set randomNumber = ? where id = ?"
-        private const val SELECT_ALL_FORTUNES = "select * from fortune"
-    }
-
-    private val dbHost: String = systemSetting("${engine.toUpperCase()}_DB_HOST", "localhost")
-
-    private val jdbcUrl: String = "jdbc:postgresql://$dbHost/$databaseName"
-
-    private val dataSource: HikariDataSource by lazy {
-        val config = HikariConfig()
-        config.jdbcUrl = jdbcUrl
-        config.maximumPoolSize = defaultSetting("maximumPoolSize", 64)
-        config.username = defaultSetting("databaseUsername", "benchmarkdbuser")
-        config.password = defaultSetting("databasePassword", "benchmarkdbpass")
-        HikariDataSource(config)
-    }
-
-    override fun findAllFortunes(): List<Fortune> {
-        val fortunes = mutableListOf<Fortune>()
-
-        dataSource.connection.use { con: Connection ->
-            val rs = con.prepareStatement(SELECT_ALL_FORTUNES).executeQuery()
-            while (rs.next())
-                fortunes += Fortune(rs.getInt(1), rs.getString(2))
-        }
-
-        return fortunes
-    }
-
-    override fun findWorlds(count: Int): List<World> {
-        val worlds: MutableList<World> = mutableListOf()
-
-        dataSource.connection.use { con: Connection ->
-            val stmtSelect = con.prepareStatement(SELECT_WORLD)
-
-            for (ii in 0 until count) {
-                stmtSelect.setInt(1, randomWorld())
-                val rs = stmtSelect.executeQuery()
-                rs.next()
-                val id = rs.getInt(1)
-                worlds += World(id, id, rs.getInt(2))
-            }
-        }
-
-        return worlds
-    }
-
-    override fun replaceWorlds(count: Int): List<World> {
-        val worlds: MutableList<World> = mutableListOf()
-
-        dataSource.connection.use { con: Connection ->
-            val stmtSelect = con.prepareStatement(SELECT_WORLD)
-            val stmtUpdate = con.prepareStatement(UPDATE_WORLD)
-
-            for (ii in 0 until count) {
-                val worldId = randomWorld()
-                val newRandomNumber = randomWorld()
-
-                stmtSelect.setInt(1, worldId)
-                val rs = stmtSelect.executeQuery()
-                rs.next()
-                rs.getInt(2) // Read 'randomNumber' to comply with Test type 5, point 6
-
-                worlds += World(worldId, worldId, newRandomNumber)
-
-                stmtUpdate.setInt(1, newRandomNumber)
-                stmtUpdate.setInt(2, worldId)
-                stmtUpdate.executeUpdate()
-            }
-        }
-
-        return worlds
-    }
-
-    override fun close() {
-        dataSource.close()
-    }
-}

+ 80 - 0
frameworks/Kotlin/hexagon/src/main/kotlin/Controller.kt

@@ -0,0 +1,80 @@
+package com.hexagonkt
+
+import com.hexagonkt.http.server.Call
+import com.hexagonkt.http.server.Router
+import com.hexagonkt.serialization.Json
+import com.hexagonkt.serialization.toFieldsMap
+import com.hexagonkt.store.BenchmarkStore
+import com.hexagonkt.templates.TemplatePort
+import java.util.concurrent.ThreadLocalRandom
+
+class Controller(private val settings: Settings) {
+
+    internal val router: Router by lazy {
+        Router {
+            before {
+                response.headers["Server"] = "Servlet/3.1"
+                response.headers["Transfer-Encoding"] = "chunked"
+            }
+
+            get("/plaintext") { ok(settings.textMessage, "text/plain") }
+            get("/json") { ok(Message(settings.textMessage), Json) }
+
+            benchmarkStores.forEach { (storeEngine, store) ->
+                benchmarkTemplateEngines.forEach { templateKind ->
+                    val path = "/$storeEngine/${templateKind.key}/fortunes"
+
+                    get(path) { listFortunes(store, templateKind.key, templateKind.value) }
+                }
+
+                get("/$storeEngine/db") { dbQuery(store) }
+                get("/$storeEngine/query") { getWorlds(store) }
+                get("/$storeEngine/cached") { getCachedWorlds(store) }
+                get("/$storeEngine/update") { updateWorlds(store) }
+            }
+        }
+    }
+
+    private fun Call.listFortunes(store: BenchmarkStore, templateKind: String, templateAdapter: TemplatePort) {
+
+        val fortunes = store.findAllFortunes() + Fortune(0, "Additional fortune added at request time.")
+        val sortedFortunes = fortunes.sortedBy { it.message }
+        val context = mapOf("fortunes" to sortedFortunes)
+
+        response.contentType = "text/html;charset=utf-8"
+        ok(templateAdapter.render("fortunes.$templateKind.html", context))
+    }
+
+    private fun Call.dbQuery(store: BenchmarkStore) {
+        ok(store.findWorlds(listOf(randomWorld())).first(), Json)
+    }
+
+    private fun Call.getWorlds(store: BenchmarkStore) {
+        val ids = (1..getWorldsCount(settings.queriesParam)).map { randomWorld() }
+        ok(store.findWorlds(ids), Json)
+    }
+
+    private fun Call.getCachedWorlds(store: BenchmarkStore) {
+        val ids = (1..getWorldsCount(settings.cachedQueriesParam)).map { randomWorld() }
+        ok(store.findCachedWorlds(ids).map { it.toFieldsMap() }, Json)
+    }
+
+    private fun Call.updateWorlds(store: BenchmarkStore) {
+        val worlds = (1..getWorldsCount(settings.queriesParam)).map { World(randomWorld(), randomWorld()) }
+        store.replaceWorlds(worlds)
+        ok(worlds, Json)
+    }
+
+    private fun Call.getWorldsCount(parameter: String): Int =
+        queryParametersValues[parameter]?.firstOrNull()?.toIntOrNull().let {
+            when {
+                it == null -> 1
+                it < 1 -> 1
+                it > 500 -> 500
+                else -> it
+            }
+        }
+
+    private fun randomWorld(): Int =
+        ThreadLocalRandom.current().nextInt(settings.worldRows) + 1
+}

+ 6 - 0
frameworks/Kotlin/hexagon/src/main/kotlin/Model.kt

@@ -0,0 +1,6 @@
+package com.hexagonkt
+
+data class Message(val message: String)
+data class Fortune(val id: Int, val message: String)
+data class World(val id: Int, val randomNumber: Int)
+data class CachedWorld(val id: Int, val randomNumber: Int)

+ 28 - 0
frameworks/Kotlin/hexagon/src/main/kotlin/Settings.kt

@@ -0,0 +1,28 @@
+package com.hexagonkt
+
+import com.hexagonkt.helpers.Jvm.systemSetting
+
+data class Settings(
+    val bindPort: Int = systemSetting("bindPort") ?: 9090,
+    val bindAddress: String = "0.0.0.0",
+
+    val database: String = "hello_world",
+    val worldCollection: String = "world",
+    val fortuneCollection: String = "fortune",
+
+    val databaseUsername: String = "benchmarkdbuser",
+    val databasePassword: String = "benchmarkdbpass",
+
+    val maximumPoolSize: Int = systemSetting("maximumPoolSize") ?: 96,
+
+    val webEngine: String = systemSetting("WEBENGINE") ?: "jetty",
+
+    val worldName: String = systemSetting("worldCollection") ?: "world",
+    val fortuneName: String = systemSetting("fortuneCollection") ?: "fortune",
+    val databaseName: String = systemSetting("database") ?: "hello_world",
+
+    val worldRows: Int = 10_000,
+    val textMessage: String = "Hello, World!",
+    val queriesParam: String = "queries",
+    val cachedQueriesParam: String = "count",
+)

+ 19 - 0
frameworks/Kotlin/hexagon/src/main/kotlin/WebListenerServer.kt

@@ -0,0 +1,19 @@
+package com.hexagonkt
+
+import com.fasterxml.jackson.module.blackbird.BlackbirdModule
+import com.hexagonkt.http.server.servlet.ServletServer
+import com.hexagonkt.serialization.JacksonMapper
+import com.hexagonkt.serialization.Json
+import com.hexagonkt.serialization.SerializationManager
+import javax.servlet.annotation.WebListener
+
+@WebListener class WebListenerServer(settings: Settings = Settings()) : ServletServer(Controller(settings).router) {
+
+    init {
+        Json.mapper.registerModule(BlackbirdModule())
+        SerializationManager.mapper = JacksonMapper
+        SerializationManager.formats = linkedSetOf(Json)
+    }
+
+    val webRouter = super.router
+}

+ 57 - 0
frameworks/Kotlin/hexagon/src/main/kotlin/store/BenchmarkMongoDbStore.kt

@@ -0,0 +1,57 @@
+package com.hexagonkt.store
+
+import com.hexagonkt.CachedWorld
+import com.hexagonkt.Fortune
+import com.hexagonkt.Settings
+import com.hexagonkt.World
+import com.hexagonkt.helpers.Jvm
+import com.hexagonkt.helpers.fail
+import com.hexagonkt.store.mongodb.MongoDbStore
+import org.cache2k.Cache
+
+internal class BenchmarkMongoDbStore(engine: String, private val settings: Settings = Settings())
+    : BenchmarkStore(settings) {
+
+    data class MongoDbWorld(val _id: Int, val id: Int, val randomNumber: Int)
+    data class MongoDbFortune(val _id: Int, val message: String)
+
+    private val dbHost: String by lazy { Jvm.systemSetting("${engine.uppercase()}_DB_HOST") ?: "localhost" }
+
+    private val dbUrl: String by lazy { "mongodb://$dbHost/${settings.databaseName}" }
+
+    private val worldRepository: MongoDbStore<MongoDbWorld, Int> by lazy {
+        MongoDbStore(MongoDbWorld::class, MongoDbWorld::_id, dbUrl, settings.worldName)
+    }
+
+    private val fortuneRepository by lazy {
+        MongoDbStore(MongoDbFortune::class, MongoDbFortune::_id, dbUrl, settings.fortuneName)
+    }
+
+    override fun findAllFortunes(): List<Fortune> = fortuneRepository.findAll().map { Fortune(it._id, it.message) }
+
+    override fun findWorlds(ids: List<Int>): List<World> =
+        ids.mapNotNull { worldRepository.findOne(it) }.map { World(it.id, it.randomNumber) }
+
+    override fun replaceWorlds(worlds: List<World>) {
+        worlds.forEach {
+            val world = worldRepository.findOne(it.id) ?: fail
+            val worldCopy = world.copy(randomNumber = it.randomNumber)
+            worldRepository.replaceOne(worldCopy)
+        }
+    }
+
+    override fun initWorldsCache(cache: Cache<Int, CachedWorld>) {
+        worldRepository.findAll().forEach {
+            cache.put(it.id, CachedWorld(it.id, it.randomNumber))
+        }
+    }
+
+    override fun loadCachedWorld(id: Int): CachedWorld =
+        worldRepository.findOne(id)
+            ?.let { world -> CachedWorld(world.id, world.randomNumber)  }
+            ?: error("World not found: $id")
+
+    override fun close() {
+        /* Not needed */
+    }
+}

+ 101 - 0
frameworks/Kotlin/hexagon/src/main/kotlin/store/BenchmarkSqlStore.kt

@@ -0,0 +1,101 @@
+package com.hexagonkt.store
+
+import com.hexagonkt.CachedWorld
+import com.hexagonkt.Fortune
+import com.hexagonkt.Settings
+import com.hexagonkt.World
+import com.hexagonkt.helpers.Jvm
+import com.zaxxer.hikari.HikariConfig
+import com.zaxxer.hikari.HikariDataSource
+import org.cache2k.Cache
+import java.sql.Connection
+import java.sql.PreparedStatement
+
+internal class BenchmarkSqlStore(engine: String, private val settings: Settings = Settings())
+    : BenchmarkStore(settings) {
+
+    companion object {
+        private const val SELECT_WORLD = "select * from world where id = ?"
+        private const val UPDATE_WORLD = "update world set randomNumber = ? where id = ?"
+        private const val SELECT_ALL_FORTUNES = "select * from fortune"
+    }
+
+    private val dbHost: String by lazy { Jvm.systemSetting("${engine.uppercase()}_DB_HOST") ?: "localhost" }
+    private val jdbcUrl: String by lazy { "jdbc:postgresql://$dbHost/${settings.databaseName}" }
+    private val dataSource: HikariDataSource by lazy {
+        val config = HikariConfig()
+        config.jdbcUrl = jdbcUrl
+        config.maximumPoolSize = Jvm.systemSetting(Int::class, "maximumPoolSize") ?: 64
+        config.username = Jvm.systemSetting("databaseUsername") ?: "benchmarkdbuser"
+        config.password = Jvm.systemSetting("databasePassword") ?: "benchmarkdbpass"
+        HikariDataSource(config)
+    }
+
+    override fun findAllFortunes(): List<Fortune> {
+        val fortunes = mutableListOf<Fortune>()
+
+        dataSource.connection.use { con: Connection ->
+            val rs = con.prepareStatement(SELECT_ALL_FORTUNES).executeQuery()
+            while (rs.next())
+                fortunes += Fortune(rs.getInt(1), rs.getString(2))
+        }
+
+        return fortunes
+    }
+
+    override fun findWorlds(ids: List<Int>): List<World> =
+        dataSource.connection.use { con: Connection ->
+            val stmtSelect = con.prepareStatement(SELECT_WORLD)
+            ids.map { con.findWorld(it, stmtSelect) }
+        }
+
+    override fun replaceWorlds(worlds: List<World>) {
+        dataSource.connection.use { con: Connection ->
+            val stmtSelect = con.prepareStatement(SELECT_WORLD)
+            val stmtUpdate = con.prepareStatement(UPDATE_WORLD)
+
+            worlds.forEach {
+                val worldId = it.id
+                val newRandomNumber = it.randomNumber
+
+                stmtSelect.setInt(1, worldId)
+                val rs = stmtSelect.executeQuery()
+                rs.next()
+                rs.getInt(2) // Read 'randomNumber' to comply with Test type 5, point 6
+
+                stmtUpdate.setInt(1, newRandomNumber)
+                stmtUpdate.setInt(2, worldId)
+                stmtUpdate.executeUpdate()
+            }
+        }
+    }
+
+    override fun initWorldsCache(cache: Cache<Int, CachedWorld>) {
+        dataSource.connection.use { con: Connection ->
+            val stmtSelect = con.prepareStatement("select * from world")
+            val rs = stmtSelect.executeQuery()
+
+            while (rs.next()) {
+                val id = rs.getInt(1)
+                val randomNumber = rs.getInt(2)
+                cache.put(id, CachedWorld(id, randomNumber))
+            }
+        }
+    }
+
+    override fun loadCachedWorld(id: Int): CachedWorld =
+        dataSource.connection.use { con ->
+            con.findWorld(id).let { world -> CachedWorld(world.id, world.randomNumber) }
+        }
+
+    override fun close() {
+        dataSource.close()
+    }
+
+    private fun Connection.findWorld(id: Int, stmtSelect: PreparedStatement = prepareStatement(SELECT_WORLD)): World {
+        stmtSelect.setInt(1, id)
+        val rs = stmtSelect.executeQuery()
+        rs.next()
+        return World(rs.getInt(1), rs.getInt(2))
+    }
+}

+ 33 - 0
frameworks/Kotlin/hexagon/src/main/kotlin/store/BenchmarkStore.kt

@@ -0,0 +1,33 @@
+package com.hexagonkt.store
+
+import com.hexagonkt.CachedWorld
+import com.hexagonkt.Fortune
+import com.hexagonkt.Settings
+import com.hexagonkt.World
+import org.cache2k.Cache
+import org.cache2k.Cache2kBuilder
+
+internal abstract class BenchmarkStore(settings: Settings) {
+
+    abstract fun findAllFortunes(): List<Fortune>
+    abstract fun findWorlds(ids: List<Int>): List<World>
+    abstract fun replaceWorlds(worlds: List<World>)
+    abstract fun initWorldsCache(cache: Cache<Int, CachedWorld>)
+    abstract fun loadCachedWorld(id: Int): CachedWorld
+    abstract fun close()
+
+    private val worldsCache: Cache<Int, CachedWorld> by lazy {
+        object : Cache2kBuilder<Int, CachedWorld>() {}
+            .eternal(true)
+            .disableMonitoring(true)
+            .disableStatistics(true)
+            .entryCapacity(settings.worldRows.toLong())
+            .loader { id -> loadCachedWorld(id) }
+            .build()
+            .apply { initWorldsCache(this) }
+    }
+
+    fun findCachedWorlds(ids: List<Int>): List<CachedWorld> {
+        return ids.mapNotNull { worldsCache.get(it) }
+    }
+}

+ 0 - 14
frameworks/Kotlin/hexagon/src/main/resources/application.yml

@@ -1,14 +0,0 @@
-
-serviceName: Hexagon Benchmark
-
-bindPort: 9090
-bindAddress: 0.0.0.0
-
-database: hello_world
-worldCollection: world
-fortuneCollection: fortune
-
-databaseUsername: benchmarkdbuser
-databasePassword: benchmarkdbpass
-
-maximumPoolSize: 96

+ 1 - 1
frameworks/Kotlin/hexagon/src/main/resources/templates/fortunes.pebble.html → frameworks/Kotlin/hexagon/src/main/resources/fortunes.pebble.html

@@ -12,7 +12,7 @@
   </tr>
   {% for fortune in fortunes %}
   <tr>
-    <td>{{ fortune._id }}</td>
+    <td>{{ fortune.id }}</td>
     <td>{{ fortune.message }}</td>
   </tr>
   {% endfor %}

+ 0 - 172
frameworks/Kotlin/hexagon/src/test/kotlin/BenchmarkTest.kt

@@ -1,172 +0,0 @@
-package com.hexagonkt
-
-import com.hexagonkt.serialization.parse
-import com.hexagonkt.http.client.Client
-import com.hexagonkt.serialization.Json
-import com.hexagonkt.http.Method.GET
-import com.hexagonkt.http.client.Response
-import com.hexagonkt.http.client.ahc.AhcAdapter
-import com.hexagonkt.http.server.jetty.JettyServletAdapter
-import com.hexagonkt.serialization.parseObjects
-import io.kotest.assertions.throwables.shouldThrow
-import io.kotest.core.spec.style.StringSpec
-import java.lang.IllegalStateException
-import java.lang.System.setProperty
-
-class BenchmarkJettyMongoDbTest : BenchmarkTestBase("jetty", "mongodb")
-
-class BenchmarkJettyPostgreSqlTest : BenchmarkTestBase("jetty", "postgresql")
-
-abstract class BenchmarkTestBase(
-    private val webEngine: String,
-    private val databaseEngine: String,
-    private val templateEngine: String = "pebble"
-): StringSpec({
-
-    val endpoint = System.getProperty("verify.endpoint")
-    val users = (System.getProperty("users") ?: "8").toInt()
-    val count = (System.getProperty("count") ?: "2").toInt() * users
-
-    lateinit var client: Client
-
-    fun checkResponse(res: Response, contentType: String) {
-        assert(res.headers["Date"] != null)
-        assert(res.headers["Server"] != null)
-        assert(res.headers["Transfer-Encoding"] != null)
-        assert(res.headers["Content-Type"]?.first() == contentType)
-    }
-
-    fun checkDbRequest(path: String, itemsCount: Int) {
-        val response = client.get(path)
-        val content = response.body
-
-        checkResponse(response, Json.contentType)
-
-        val resultsList = content?.parse(List::class) ?: error("")
-        assert(itemsCount == resultsList.size)
-
-        (1..itemsCount).forEach {
-            val r = resultsList[it - 1] as Map<*, *>
-            assert(r.containsKey(World::id.name) && r.containsKey(World::randomNumber.name))
-            assert(!r.containsKey(World::_id.name))
-            assert((r[World::id.name] as Int) in 1..10000)
-        }
-    }
-
-    beforeSpec {
-        setProperty("WEBENGINE", webEngine)
-        client = if (endpoint == null) {
-            main()
-            Client(AhcAdapter(), "http://localhost:${benchmarkServer.runtimePort}")
-        }
-        else {
-            Client(AhcAdapter(), endpoint)
-        }
-    }
-
-    afterSpec {
-        benchmarkStores[databaseEngine]?.close()
-        benchmarkServer.stop()
-    }
-
-    "Empty server code creates a Jetty Servlet Adapter" {
-        System.clearProperty("WEBENGINE")
-        createEngine()
-        assert(engine is JettyServletAdapter)
-    }
-
-    "Invalid server code throws an exception" {
-        shouldThrow<IllegalStateException> {
-            setProperty("WEBENGINE", "invalid")
-            createEngine()
-        }
-    }
-
-    "Web" {
-        val web = Web()
-
-        val webRoutes = web.serverRouter.requestHandlers
-            .map { it.route.methods.first() to it.route.path.pattern }
-
-        val benchmarkRoutes = listOf(
-            GET to "/plaintext",
-            GET to "/json",
-            GET to "/$databaseEngine/$templateEngine/fortunes",
-            GET to "/$databaseEngine/db",
-            GET to "/$databaseEngine/query",
-            GET to "/$databaseEngine/update"
-        )
-
-        assert(webRoutes.containsAll(benchmarkRoutes))
-    }
-
-    "JSON".config(invocations = count, threads = users) {
-        val response = client.get("/json")
-        val content = response.body
-
-        checkResponse(response, Json.contentType)
-        assert("Hello, World!" == content?.parse<Message>()?.message)
-    }
-
-    "Plaintext".config(invocations = count, threads = users) {
-        val response = client.get("/plaintext")
-        val content = response.body
-
-        checkResponse(response, "text/plain")
-        assert("Hello, World!" == content)
-    }
-
-    "Fortunes".config(invocations = count, threads = users) {
-        val response = client.get("/$databaseEngine/$templateEngine/fortunes")
-        val content = response.body ?: error("body is required")
-
-        checkResponse(response, "text/html;charset=utf-8")
-        assert(content.contains("<td>&lt;script&gt;alert(&quot;This should not be"))
-        assert(content.contains(" displayed in a browser alert box.&quot;);&lt;/script&gt;</td>"))
-        assert(content.contains("<td>フレームワークのベンチマーク</td>"))
-    }
-
-    "No query parameter".config(invocations = count, threads = users) {
-        val response = client.get("/$databaseEngine/db")
-        val body = response.body ?: error("body is required")
-
-        checkResponse(response, Json.contentType)
-        val bodyMap = body.parse(Map::class)
-        assert(bodyMap.containsKey(World::id.name))
-        assert(bodyMap.containsKey(World::randomNumber.name))
-    }
-
-    "No updates parameter".config(invocations = count, threads = users) {
-        val response = client.get("/$databaseEngine/update")
-        val body = response.body ?: error("body is required")
-
-        checkResponse(response, Json.contentType)
-        val bodyMap = body.parseObjects(Map::class).first()
-        assert(bodyMap.containsKey(World::id.name))
-        assert(bodyMap.containsKey(World::randomNumber.name))
-    }
-
-    fun testDb(test: String, path: String, itemsCount: Int) {
-        test.config(invocations = count, threads = users) {
-            checkDbRequest("/$databaseEngine/$path", itemsCount)
-        }
-    }
-
-    testDb("Empty query parameter", "query?queries", 1)
-    testDb("Text query parameter", "query?queries=text", 1)
-    testDb("Zero queries", "query?queries=0", 1)
-    testDb("One thousand queries", "query?queries=1000", 500)
-    testDb("One query", "query?queries=1", 1)
-    testDb("Ten queries", "query?queries=10", 10)
-    testDb("One hundred queries", "query?queries=100", 100)
-    testDb("Five hundred queries", "query?queries=500", 500)
-
-    testDb("Empty updates parameter", "update?queries", 1)
-    testDb("Text updates parameter", "update?queries=text", 1)
-    testDb("Zero updates", "update?queries=0", 1)
-    testDb("One thousand updates", "update?queries=1000", 500)
-    testDb("One update", "update?queries=1", 1)
-    testDb("Ten updates", "update?queries=10", 10)
-    testDb("One hundred updates", "update?queries=100", 100)
-    testDb("Five hundred updates", "update?queries=500", 500)
-})

+ 0 - 3
frameworks/Kotlin/hexagon/src/test/resources/application_test.yml

@@ -1,3 +0,0 @@
-
-bindPort: 0
-maximumPoolSize: 8