diff --git a/frameworks/Kotlin/ktor/Readme.md b/frameworks/Kotlin/ktor/Readme.md index 80366c2f893..a0e83a1e29e 100644 --- a/frameworks/Kotlin/ktor/Readme.md +++ b/frameworks/Kotlin/ktor/Readme.md @@ -1,4 +1,7 @@ See subprojects * [Ktor](ktor/) Ktor using traditional JDBC using various servers -* [Ktor-asyncdb](ktor-asyncdb/) Ktor with Netty-based PostgreSQL clients \ No newline at end of file +* [Ktor-jasync](ktor-asyncdb/) Ktor with Netty-based PostgreSQL clients +* [Ktor-r2dbc](ktor-r2dbc/) Ktor with R2DBC for reactive database access +* [Ktor-exposed](ktor-exposed/) Ktor with the Exposed database abstraction library +* [Ktor-pgclient](ktor-pgclient/) Ktor with Reactive PostgreSQL Client \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/benchmark_config.json b/frameworks/Kotlin/ktor/benchmark_config.json index 5a9b1816736..17373df20ee 100644 --- a/frameworks/Kotlin/ktor/benchmark_config.json +++ b/frameworks/Kotlin/ktor/benchmark_config.json @@ -101,7 +101,7 @@ "query_url": "/query/?queries=", "fortune_url": "/fortunes", "update_url": "/updates?queries=", - "port": 8080, + "port": 9090, "approach": "Realistic", "classification": "Fullstack", "database": "Postgres", @@ -125,7 +125,7 @@ "update_url": "/updates?queries=", "fortune_url": "/fortunes", - "port": 8080, + "port": 9090, "approach": "Realistic", "classification": "Micro", "database": "Postgres", @@ -145,7 +145,7 @@ "query_url": "/queries?queries=", "update_url": "/updates?queries=", "fortune_url": "/fortunes", - "port": 8080, + "port": 9090, "approach": "Realistic", "classification": "Micro", "database": "postgres", @@ -164,7 +164,7 @@ "exposed-dao": { "db_url": "/db", "fortune_url": "/fortunes", - "port": 8080, + "port": 9090, "approach": "Realistic", "classification": "Micro", "database": "postgres", diff --git a/frameworks/Kotlin/ktor/ktor-asyncdb/build.gradle.kts b/frameworks/Kotlin/ktor/ktor-asyncdb/build.gradle.kts index 029a0973171..0622963101a 100644 --- a/frameworks/Kotlin/ktor/ktor-asyncdb/build.gradle.kts +++ b/frameworks/Kotlin/ktor/ktor-asyncdb/build.gradle.kts @@ -13,11 +13,11 @@ repositories { } application { - mainClass.set("MainKt") + mainClass = "io.ktor.server.netty.EngineMain" } -val ktor_version = "2.3.12" -val kotlinx_serialization_version = "1.7.3" +val ktor_version = "3.1.2" +val kotlinx_serialization_version = "1.8.1" dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_version") @@ -26,6 +26,7 @@ dependencies { implementation("io.ktor:ktor-server-default-headers:$ktor_version") implementation("io.ktor:ktor-server-html-builder:$ktor_version") implementation("com.github.jasync-sql:jasync-postgresql:2.2.4") + implementation("ch.qos.logback:logback-classic:1.5.12") } java { diff --git a/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/Models.kt b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/Models.kt new file mode 100644 index 00000000000..72db920aefb --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/Models.kt @@ -0,0 +1,10 @@ +import kotlinx.serialization.Serializable + +@Serializable +data class Message(val message: String) + +@Serializable +data class World(val id: Int, var randomNumber: Int) + +@Serializable +data class Fortune(val id: Int, var message: String) \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/Responses.kt b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/Responses.kt new file mode 100644 index 00000000000..9a1a92b7071 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/Responses.kt @@ -0,0 +1,20 @@ +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.json.Json + +// Optimized JSON instance with better performance settings +internal val json = Json { + prettyPrint = false + isLenient = true + ignoreUnknownKeys = true + coerceInputValues = true +} + +internal suspend inline fun RoutingCall.respondJson(response: E) { + respond(TextContent( + json.encodeToString(response), + ContentType.Application.Json + )) +} \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/main.kt b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/main.kt index e9f4221d8ed..488dced6cec 100644 --- a/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/main.kt +++ b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/kotlin/main.kt @@ -3,31 +3,16 @@ import com.github.jasync.sql.db.QueryResult import com.github.jasync.sql.db.SuspendingConnection import com.github.jasync.sql.db.asSuspending import com.github.jasync.sql.db.postgresql.PostgreSQLConnectionBuilder -import io.ktor.http.ContentType import io.ktor.server.application.* -import io.ktor.server.engine.embeddedServer import io.ktor.server.html.* -import io.ktor.server.netty.Netty import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.coroutines.* import kotlinx.html.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import java.lang.IllegalArgumentException import kotlin.random.Random import kotlin.random.nextInt -@Serializable -data class Message(val message: String) - -@Serializable -data class World(val id: Int, val randomNumber: Int) - -data class Fortune(val id: Int, val message: String) - val rand = Random(1) interface Repository { @@ -121,57 +106,46 @@ class FortuneTemplate(private val fortunes: List, private val main: Mai } } -fun main(args: Array) { - val db = when(args.firstOrNull()) { - "jasync-sql" -> JasyncRepository() - else -> throw IllegalArgumentException("Must specify a postgres client") - } +fun Application.main() { - val server = embeddedServer(Netty, 8080, configure = { - shareWorkGroup = true - }) { - install(DefaultHeaders) - routing { - get("/plaintext") { - call.respondText("Hello, World!") - } + val db = JasyncRepository() - get("/json") { - call.respondText( - Json.encodeToString(Message("Hello, World!")), - ContentType.Application.Json - ) - } + install(DefaultHeaders) + routing { + get("/plaintext") { + call.respondText("Hello, World!") + } - get("/db") { - call.respondText(Json.encodeToString(db.getWorld()), ContentType.Application.Json) - } + get("/json") { + call.respondJson(Message("Hello, World!")) + } - get("/query/") { - val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 - val worlds = (1..queries).map { db.getWorld() } - call.respondText(Json.encodeToString(worlds), ContentType.Application.Json) - } + get("/db") { + call.respondJson(db.getWorld()) + } - get("/fortunes") { - val newFortune = Fortune(0, "Additional fortune added at request time.") - val fortunes = db.getFortunes().toMutableList() - fortunes.add(newFortune) - fortunes.sortBy { it.message } - call.respondHtmlTemplate(FortuneTemplate(fortunes)) { } - } + get("/query/") { + val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 + val worlds = (1..queries).map { db.getWorld() } + call.respondJson(worlds) + } - get("/updates") { - val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 - val worlds = (1..queries).map { db.getWorld() } - val newWorlds = worlds.map { it.copy(randomNumber = rand.nextInt(1..10000)) } + get("/fortunes") { + val newFortune = Fortune(0, "Additional fortune added at request time.") + val fortunes = db.getFortunes().toMutableList() + fortunes.add(newFortune) + fortunes.sortBy { it.message } + call.respondHtmlTemplate(FortuneTemplate(fortunes)) { } + } - db.updateWorlds(newWorlds) + get("/updates") { + val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 + val worlds = (1..queries).map { db.getWorld() } + val newWorlds = worlds.map { it.copy(randomNumber = rand.nextInt(1..10000)) } - call.respondText(Json.encodeToString(newWorlds), ContentType.Application.Json) - } + db.updateWorlds(newWorlds) + + call.respondJson(newWorlds) } } - - server.start(wait = true) } diff --git a/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/resources/application.conf b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/resources/application.conf new file mode 100644 index 00000000000..ad95fb429f0 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/resources/application.conf @@ -0,0 +1,12 @@ +ktor { + deployment { + port = 9090 + autoreload = false + watch = [ ] + shareWorkGroup = true + } + + application { + modules = [MainKt.main] + } +} diff --git a/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/resources/logback.xml b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/resources/logback.xml new file mode 100644 index 00000000000..9fd0f518971 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-asyncdb/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + true + + + + + + + + + + + + diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts b/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts index 63db97d7dd2..56398c64112 100644 --- a/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts @@ -9,7 +9,7 @@ repositories { mavenCentral() } -val ktorVersion = "3.1.1" +val ktorVersion = "3.1.2" val kotlinxSerializationVersion = "1.8.0" val exposedVersion = "0.59.0" diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/App.kt b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/App.kt index 513a20b7ab7..50ffd37975d 100644 --- a/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/App.kt +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/App.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.html.* import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass @@ -70,7 +69,7 @@ enum class ExposedMode { fun main(args: Array) { val exposedMode = valueOf(args.first()) - embeddedServer(Netty, port = 8080) { module(exposedMode) }.start(wait = true) + embeddedServer(Netty, port = 9090) { module(exposedMode) }.start(wait = true) } fun Application.module(exposedMode: ExposedMode) { diff --git a/frameworks/Kotlin/ktor/ktor-pgclient.dockerfile b/frameworks/Kotlin/ktor/ktor-pgclient.dockerfile index 301f5e55c31..dfee699ede7 100644 --- a/frameworks/Kotlin/ktor/ktor-pgclient.dockerfile +++ b/frameworks/Kotlin/ktor/ktor-pgclient.dockerfile @@ -10,6 +10,6 @@ FROM amazoncorretto:21-al2023-headless WORKDIR /app COPY --from=build /app/build/libs/ktor-pgclient.jar ktor-pgclient.jar -EXPOSE 8080 +EXPOSE 9090 CMD ["java", "-server","-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "ktor-pgclient.jar"] diff --git a/frameworks/Kotlin/ktor/ktor-pgclient/README.md b/frameworks/Kotlin/ktor/ktor-pgclient/README.md index a1e9827a659..024a9950fc6 100755 --- a/frameworks/Kotlin/ktor/ktor-pgclient/README.md +++ b/frameworks/Kotlin/ktor/ktor-pgclient/README.md @@ -7,27 +7,27 @@ The client features batching, pipelining and supports coroutines. ### Plain Text Test - http://localhost:8080/plaintext + http://localhost:9090/plaintext ### JSON Encoding Test - http://localhost:8080/json + http://localhost:9090/json ### Single Query Test - http://localhost:8080/db + http://localhost:9090/db ### Multiple Queries Test - http://localhost:8080/query?queries= + http://localhost:9090/query?queries= ### Database updates Test - http://localhost:8080/updates?queries= + http://localhost:9090/updates?queries= ### Fortunes Test - http://localhost:8080/fortunes + http://localhost:9090/fortunes ## build diff --git a/frameworks/Kotlin/ktor/ktor-pgclient/build.gradle.kts b/frameworks/Kotlin/ktor/ktor-pgclient/build.gradle.kts index 60524844558..c98ef463979 100644 --- a/frameworks/Kotlin/ktor/ktor-pgclient/build.gradle.kts +++ b/frameworks/Kotlin/ktor/ktor-pgclient/build.gradle.kts @@ -13,20 +13,21 @@ repositories { } application { - mainClass.set("MainKt") + mainClass = "io.ktor.server.netty.EngineMain" } -val ktor_version = "2.3.12" +val ktor_version = "3.1.2" val vertx_version = "4.5.11" dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") implementation("io.ktor:ktor-server-netty:$ktor_version") implementation("io.ktor:ktor-server-html-builder-jvm:$ktor_version") implementation("io.ktor:ktor-server-default-headers-jvm:$ktor_version") implementation("io.vertx:vertx-pg-client:$vertx_version") implementation("io.vertx:vertx-lang-kotlin:$vertx_version") implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertx_version") + implementation("ch.qos.logback:logback-classic:1.5.12") } java { diff --git a/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/Models.kt b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/Models.kt new file mode 100644 index 00000000000..72db920aefb --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/Models.kt @@ -0,0 +1,10 @@ +import kotlinx.serialization.Serializable + +@Serializable +data class Message(val message: String) + +@Serializable +data class World(val id: Int, var randomNumber: Int) + +@Serializable +data class Fortune(val id: Int, var message: String) \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/Responses.kt b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/Responses.kt new file mode 100644 index 00000000000..9a1a92b7071 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/Responses.kt @@ -0,0 +1,20 @@ +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.json.Json + +// Optimized JSON instance with better performance settings +internal val json = Json { + prettyPrint = false + isLenient = true + ignoreUnknownKeys = true + coerceInputValues = true +} + +internal suspend inline fun RoutingCall.respondJson(response: E) { + respond(TextContent( + json.encodeToString(response), + ContentType.Application.Json + )) +} \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/main.kt b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/main.kt index c8e74244f99..1afafc28fcd 100644 --- a/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/main.kt +++ b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/main.kt @@ -1,32 +1,16 @@ -import io.ktor.http.* import io.ktor.server.application.* -import io.ktor.server.engine.* import io.ktor.server.html.* -import io.ktor.server.netty.* import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.response.* import io.ktor.server.routing.* -import io.vertx.kotlin.coroutines.await import io.vertx.kotlin.coroutines.coAwait import io.vertx.pgclient.PgBuilder import io.vertx.pgclient.PgConnectOptions -import io.vertx.pgclient.PgPool import io.vertx.sqlclient.PoolOptions import io.vertx.sqlclient.Tuple import kotlinx.html.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import java.util.concurrent.ThreadLocalRandom -@Serializable -data class Message(val message: String) - -@Serializable -data class World(val id: Int, val randomNumber: Int) - -data class Fortune(val id: Int, val message: String) - val rand: ThreadLocalRandom get() = ThreadLocalRandom.current() @@ -125,54 +109,45 @@ class FortuneTemplate( } } -fun main() { +fun Application.main() { val db = PgclientRepository() - val server = embeddedServer(Netty, 8080, configure = { - shareWorkGroup = true - }) { - install(DefaultHeaders) - routing { - get("/plaintext") { - call.respondText("Hello, World!") - } + install(DefaultHeaders) + routing { + get("/plaintext") { + call.respondText("Hello, World!") + } - get("/json") { - call.respondText( - Json.encodeToString(Message("Hello, World!")), - ContentType.Application.Json - ) - } + get("/json") { + call.respondJson(Message("Hello, World!")) + } - get("/db") { - call.respondText(Json.encodeToString(db.getWorld()), ContentType.Application.Json) - } + get("/db") { + call.respondJson(db.getWorld()) + } - get("/query") { - val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 - val worlds = List(queries) { db.getWorld() } - call.respondText(Json.encodeToString(worlds), ContentType.Application.Json) - } + get("/query") { + val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 + val worlds = List(queries) { db.getWorld() } + call.respondJson(worlds) + } - get("/fortunes") { - val newFortune = Fortune(0, "Additional fortune added at request time.") - val fortunes = db.getFortunes().toMutableList() - fortunes.add(newFortune) - fortunes.sortBy { it.message } - call.respondHtmlTemplate(FortuneTemplate(fortunes)) { } - } + get("/fortunes") { + val newFortune = Fortune(0, "Additional fortune added at request time.") + val fortunes = db.getFortunes().toMutableList() + fortunes.add(newFortune) + fortunes.sortBy { it.message } + call.respondHtmlTemplate(FortuneTemplate(fortunes)) { } + } - get("/updates") { - val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 - val worlds = List(queries) { db.getWorld() } - val newWorlds = worlds.map { it.copy(randomNumber = rand.nextInt(1, 10001)) } + get("/updates") { + val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1 + val worlds = List(queries) { db.getWorld() } + val newWorlds = worlds.map { it.copy(randomNumber = rand.nextInt(1, 10001)) } - db.updateWorlds(newWorlds) + db.updateWorlds(newWorlds) - call.respondText(Json.encodeToString(newWorlds), ContentType.Application.Json) - } + call.respondJson(newWorlds) } } - - server.start(wait = true) } diff --git a/frameworks/Kotlin/ktor/ktor-pgclient/src/main/resources/application.conf b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/resources/application.conf new file mode 100644 index 00000000000..ad95fb429f0 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/resources/application.conf @@ -0,0 +1,12 @@ +ktor { + deployment { + port = 9090 + autoreload = false + watch = [ ] + shareWorkGroup = true + } + + application { + modules = [MainKt.main] + } +} diff --git a/frameworks/Kotlin/ktor/ktor-pgclient/src/main/resources/logback.xml b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/resources/logback.xml new file mode 100644 index 00000000000..9fd0f518971 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-pgclient/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + true + + + + + + + + + + + + diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc/pom.xml b/frameworks/Kotlin/ktor/ktor-r2dbc/pom.xml index 75b1efc4cc0..cffd5da8488 100644 --- a/frameworks/Kotlin/ktor/ktor-r2dbc/pom.xml +++ b/frameworks/Kotlin/ktor/ktor-r2dbc/pom.xml @@ -14,8 +14,8 @@ 2.1.20 1.10.1 - 3.1.1 - 1.8.0 + 3.1.2 + 1.8.1 0.12.0 UTF-8 1.5.12 @@ -42,6 +42,11 @@ kotlinx-serialization-json ${serialization.version} + + org.jetbrains.kotlinx + kotlinx-serialization-json-io + ${serialization.version} + org.jetbrains.kotlinx kotlinx-html-jvm diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt index ac5d03fdbe8..b0a05351157 100644 --- a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt +++ b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt @@ -4,7 +4,7 @@ import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.application.* import io.ktor.server.config.* -import io.ktor.server.html.* +import io.ktor.server.html.respondHtml import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -20,35 +20,25 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.reactive.awaitFirst import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.html.* -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.jetbrains.ktor.benchmarks.Constants.DB_ROWS -import org.jetbrains.ktor.benchmarks.Constants.FORTUNES_QUERY -import org.jetbrains.ktor.benchmarks.Constants.UPDATE_QUERY -import org.jetbrains.ktor.benchmarks.Constants.WORLD_QUERY -import org.jetbrains.ktor.benchmarks.models.Fortune -import org.jetbrains.ktor.benchmarks.models.Message -import org.jetbrains.ktor.benchmarks.models.World import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import kotlin.random.Random import java.time.Duration +import kotlin.random.Random -private val json = Json { - prettyPrint = false - isLenient = true - ignoreUnknownKeys = true - coerceInputValues = true -} +const val HELLO_WORLD = "Hello, World!" +const val WORLD_QUERY = "SELECT id, randomnumber FROM world WHERE id = $1" +const val FORTUNES_QUERY = "SELECT id, message FROM fortune" +const val UPDATE_QUERY = "UPDATE world SET randomnumber = $1 WHERE id = $2" +const val DB_ROWS = 10000 fun Application.main() { val config = ApplicationConfig("application.conf") val dbConnFactory = configurePostgresR2DBC(config) - install(DefaultHeaders) - val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain) - val helloWorldMsg = Message("Hello, world!") + val random = Random.Default + + install(DefaultHeaders) routing { get("/plaintext") { @@ -56,15 +46,14 @@ fun Application.main() { } get("/json") { - call.respondText(json.encodeToString(helloWorldMsg), ContentType.Application.Json) + call.respondJson(Message(HELLO_WORLD)) } get("/db") { - val random = Random.Default val request = getWorld(dbConnFactory, random) val result = request.awaitFirstOrNull() - call.respondText(json.encodeToString(result), ContentType.Application.Json) + call.respondJson(result) } fun selectWorlds(queries: Int, random: Random): Flow = flow { @@ -75,7 +64,6 @@ fun Application.main() { get("/queries") { val queries = call.queries() - val random = Random.Default val result = buildList { selectWorlds(queries, random).collect { @@ -83,7 +71,7 @@ fun Application.main() { } } - call.respondText(json.encodeToString(result), ContentType.Application.Json) + call.respondJson(result) } get("/fortunes") { @@ -124,7 +112,6 @@ fun Application.main() { get("/updates") { val queries = call.queries() - val random = Random.Default val worlds = selectWorlds(queries, random) @@ -144,7 +131,7 @@ fun Application.main() { } } - call.respondText(json.encodeToString(worldsUpdated), ContentType.Application.Json) + call.respondJson(worldsUpdated) } } } @@ -194,12 +181,4 @@ private fun configurePostgresR2DBC(config: ApplicationConfig): ConnectionFactory return ConnectionPool(cp) } -private fun ApplicationCall.queries() = request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1 - - -object Constants { - const val WORLD_QUERY = "SELECT id, randomnumber FROM world WHERE id = $1" - const val FORTUNES_QUERY = "SELECT id, message FROM fortune" - const val UPDATE_QUERY = "UPDATE world SET randomnumber = $1 WHERE id = $2" - const val DB_ROWS = 10000 -} +private fun ApplicationCall.queries() = request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1 \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Models.kt b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Models.kt new file mode 100644 index 00000000000..b4325257282 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Models.kt @@ -0,0 +1,12 @@ +package org.jetbrains.ktor.benchmarks + +import kotlinx.serialization.Serializable + +@Serializable +data class Message(val message: String) + +@Serializable +data class World(val id: Int, var randomNumber: Int) + +@Serializable +data class Fortune(val id: Int, var message: String) \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Responses.kt b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Responses.kt new file mode 100644 index 00000000000..07f21a06cf0 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Responses.kt @@ -0,0 +1,22 @@ +package org.jetbrains.ktor.benchmarks + +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.json.Json + +// Optimized JSON instance with better performance settings +internal val json = Json { + prettyPrint = false + isLenient = true + ignoreUnknownKeys = true + coerceInputValues = true +} + +internal suspend inline fun RoutingCall.respondJson(response: E) { + respond(TextContent( + json.encodeToString(response), + ContentType.Application.Json + )) +} \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Fortune.kt b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Fortune.kt deleted file mode 100644 index 9484e01ab95..00000000000 --- a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Fortune.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.jetbrains.ktor.benchmarks.models - -import kotlinx.serialization.Serializable - -@Serializable -data class Fortune(val id: Int, var message: String) diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Message.kt b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Message.kt deleted file mode 100644 index 916169fbaf8..00000000000 --- a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Message.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.jetbrains.ktor.benchmarks.models - -import kotlinx.serialization.Serializable - -@Serializable -data class Message(val message: String) - -// Cache common messages to reduce allocations -object MessageCache { - val helloWorld = Message("Hello, world!") -} diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/World.kt b/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/World.kt deleted file mode 100644 index 8b44731ccdf..00000000000 --- a/frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/World.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.jetbrains.ktor.benchmarks.models - -import kotlinx.serialization.Serializable - -@Serializable -data class World(val id: Int, var randomNumber: Int) diff --git a/frameworks/Kotlin/ktor/ktor/README.md b/frameworks/Kotlin/ktor/ktor/README.md index 31f2755e24c..02a13760b07 100644 --- a/frameworks/Kotlin/ktor/ktor/README.md +++ b/frameworks/Kotlin/ktor/ktor/README.md @@ -39,6 +39,8 @@ Please note that the server holds tty so you may need nohup. See `setup.sh` for [Leonid Stashevsky](https://github.com/e5l) +[Bruce Hamilton](https://github.com/bjhham) + [Sergey Mashkov](https://github.com/cy6erGn0m) [Ilya Ryzhenkov](https://github.com/orangy) diff --git a/frameworks/Kotlin/ktor/ktor/pom.xml b/frameworks/Kotlin/ktor/ktor/pom.xml index 93c927a496c..1ae4aa0b3d5 100644 --- a/frameworks/Kotlin/ktor/ktor/pom.xml +++ b/frameworks/Kotlin/ktor/ktor/pom.xml @@ -13,12 +13,12 @@ 2.1.20 - 3.1.1 - 1.8.0 + 3.1.2 + 1.8.1 0.12.0 UTF-8 5.1.0 - 1.2.13 + 1.5.12 8.0.33 42.7.4 @@ -39,6 +39,11 @@ kotlinx-serialization-json ${serialization.version} + + org.jetbrains.kotlinx + kotlinx-serialization-json-io + ${serialization.version} + org.jetbrains.kotlinx kotlinx-html-jvm diff --git a/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Database.kt b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Database.kt new file mode 100644 index 00000000000..ee57ad5126d --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Database.kt @@ -0,0 +1,32 @@ +package org.jetbrains.ktor.benchmarks + +import com.zaxxer.hikari.HikariConfig + +fun HikariConfig.configurePostgres(poolSize: Int) { + jdbcUrl = "jdbc:postgresql://tfb-database/hello_world?useSSL=false" + driverClassName = org.postgresql.Driver::class.java.name + configureCommon(poolSize) +} + +fun HikariConfig.configureCommon(poolSize: Int) { + username = "benchmarkdbuser" + password = "benchmarkdbpass" + addDataSourceProperty("cacheServerConfiguration", true) + addDataSourceProperty("cachePrepStmts", "true") + addDataSourceProperty("useUnbufferedInput", "false") + addDataSourceProperty("prepStmtCacheSize", "4096") + addDataSourceProperty("prepStmtCacheSqlLimit", "2048") + connectionTimeout = 5000 + maximumPoolSize = poolSize + minimumIdle = poolSize + idleTimeout = 300000 // 5 minutes + maxLifetime = 600000 // 10 minutes + validationTimeout = 5000 + leakDetectionThreshold = 60000 +} + +fun HikariConfig.configureMySql(poolSize: Int) { + jdbcUrl = "jdbc:mysql://tfb-database:3306/hello_world?useSSL=false" + driverClassName = com.mysql.jdbc.Driver::class.java.name + configureCommon(poolSize) +} \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt deleted file mode 100644 index 17e8a8d8429..00000000000 --- a/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt +++ /dev/null @@ -1,203 +0,0 @@ -package org.jetbrains.ktor.benchmarks - -import com.zaxxer.hikari.HikariConfig -import com.zaxxer.hikari.HikariDataSource -import io.ktor.http.* -import io.ktor.http.content.* -import io.ktor.server.application.* -import io.ktor.server.html.* -import io.ktor.server.plugins.defaultheaders.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import kotlinx.coroutines.* -import kotlinx.html.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.jetbrains.ktor.benchmarks.Constants.FORTUNES_QUERY -import org.jetbrains.ktor.benchmarks.Constants.UPDATE_QUERY -import org.jetbrains.ktor.benchmarks.Constants.WORLD_QUERY -import java.sql.Connection -import java.util.concurrent.ThreadLocalRandom -import kotlin.random.Random -import kotlinx.serialization.Contextual - -@Serializable -data class Message(val message: String) - -@Serializable -data class World(val id: Int, var randomNumber: Int) - -@Serializable -data class Fortune(val id: Int, var message: String) - -// Optimized JSON instance with better performance settings -private val json = Json { - prettyPrint = false - isLenient = true - ignoreUnknownKeys = true - coerceInputValues = true -} - -fun Application.main() { - val dbRows = 10000 - val poolSize = Runtime.getRuntime().availableProcessors() * 2 - val pool = HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) }) - - // Create a dedicated dispatcher for database operations - val databaseDispatcher = Dispatchers.IO.limitedParallelism(poolSize) - - install(DefaultHeaders) - - val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain) - val jsonResponse = json.encodeToString(Message("Hello, world!")) - - routing { - get("/plaintext") { - call.respond(helloWorldContent) - } - - get("/json") { - call.respondText(jsonResponse, ContentType.Application.Json) - } - - get("/db") { - val random = Random.Default - - val world = withContext(databaseDispatcher) { - pool.connection.use { connection -> - connection.prepareStatement(WORLD_QUERY).use { statement -> - statement.setInt(1, random.nextInt(dbRows) + 1) - statement.executeQuery().use { rs -> - rs.next() - World(rs.getInt(1), rs.getInt(2)) - } - } - } - } - - call.respondText(json.encodeToString(world), ContentType.Application.Json) - } - - fun Connection.selectWorlds(queries: Int, random: Random): List { - val result = ArrayList(queries) - prepareStatement(WORLD_QUERY).use { statement -> - repeat(queries) { - statement.setInt(1, random.nextInt(dbRows) + 1) - statement.executeQuery().use { rs -> - rs.next() - result += World(rs.getInt(1), rs.getInt(2)) - } - } - } - return result - } - - get("/queries") { - val queries = call.queries() - val random = Random.Default - - val result = withContext(databaseDispatcher) { - pool.connection.use { it.selectWorlds(queries, random) } - } - - call.respondText(json.encodeToString(result), ContentType.Application.Json) - } - - get("/fortunes") { - val result = mutableListOf() - withContext(databaseDispatcher) { - pool.connection.use { connection -> - connection.prepareStatement(FORTUNES_QUERY).use { statement -> - statement.executeQuery().use { rs -> - while (rs.next()) { - result += Fortune(rs.getInt(1), rs.getString(2)) - } - } - } - } - } - result.add(Fortune(0, "Additional fortune added at request time.")) - result.sortBy { it.message } - call.respondHtml { - head { title { +"Fortunes" } } - body { - table { - tr { - th { +"id" } - th { +"message" } - } - for (fortune in result) { - tr { - td { +fortune.id.toString() } - td { +fortune.message } - } - } - } - } - } - } - - get("/updates") { - val queries = call.queries() - val random = Random.Default - val result: List - - withContext(databaseDispatcher) { - pool.connection.use { connection -> - result = connection.selectWorlds(queries, random) - result.forEach { it.randomNumber = random.nextInt(dbRows) + 1 } - - connection.prepareStatement(UPDATE_QUERY).use { updateStatement -> - for ((id, randomNumber) in result) { - updateStatement.setInt(1, randomNumber) - updateStatement.setInt(2, id) - updateStatement.addBatch() - } - updateStatement.executeBatch() - } - } - } - - call.respondText(json.encodeToString(result), ContentType.Application.Json) - } - } -} - -fun HikariConfig.configurePostgres(poolSize: Int) { - jdbcUrl = "jdbc:postgresql://tfb-database/hello_world?useSSL=false" - driverClassName = org.postgresql.Driver::class.java.name - configureCommon(poolSize) -} - -fun HikariConfig.configureCommon(poolSize: Int) { - username = "benchmarkdbuser" - password = "benchmarkdbpass" - addDataSourceProperty("cacheServerConfiguration", true) - addDataSourceProperty("cachePrepStmts", "true") - addDataSourceProperty("useUnbufferedInput", "false") - addDataSourceProperty("prepStmtCacheSize", "4096") - addDataSourceProperty("prepStmtCacheSqlLimit", "2048") - connectionTimeout = 5000 - maximumPoolSize = poolSize - minimumIdle = poolSize - idleTimeout = 300000 // 5 minutes - maxLifetime = 600000 // 10 minutes - validationTimeout = 5000 - leakDetectionThreshold = 60000 -} - -fun HikariConfig.configureMySql(poolSize: Int) { - jdbcUrl = "jdbc:mysql://tfb-database:3306/hello_world?useSSL=false" - driverClassName = com.mysql.jdbc.Driver::class.java.name - configureCommon(poolSize) -} - -fun ApplicationCall.queries() = - request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1 - -object Constants { - const val WORLD_QUERY = "SELECT id, randomNumber FROM World WHERE id = ?" - const val FORTUNES_QUERY = "SELECT id, message FROM fortune" - const val UPDATE_QUERY = "UPDATE World SET randomNumber = ? WHERE id = ?" -} \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Models.kt b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Models.kt new file mode 100644 index 00000000000..b4325257282 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Models.kt @@ -0,0 +1,12 @@ +package org.jetbrains.ktor.benchmarks + +import kotlinx.serialization.Serializable + +@Serializable +data class Message(val message: String) + +@Serializable +data class World(val id: Int, var randomNumber: Int) + +@Serializable +data class Fortune(val id: Int, var message: String) \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Responses.kt b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Responses.kt new file mode 100644 index 00000000000..07f21a06cf0 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Responses.kt @@ -0,0 +1,22 @@ +package org.jetbrains.ktor.benchmarks + +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.json.Json + +// Optimized JSON instance with better performance settings +internal val json = Json { + prettyPrint = false + isLenient = true + ignoreUnknownKeys = true + coerceInputValues = true +} + +internal suspend inline fun RoutingCall.respondJson(response: E) { + respond(TextContent( + json.encodeToString(response), + ContentType.Application.Json + )) +} \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/main.kt b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/main.kt new file mode 100644 index 00000000000..eea3566b953 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/main.kt @@ -0,0 +1,151 @@ +package org.jetbrains.ktor.benchmarks + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.application.* +import io.ktor.server.html.respondHtml +import io.ktor.server.plugins.defaultheaders.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.withContext +import kotlinx.html.* +import java.sql.Connection +import java.util.StringJoiner +import kotlin.random.Random + +const val HELLO_WORLD = "Hello, World!" +const val WORLD_QUERY = "SELECT id, randomNumber FROM World WHERE id = ?" +const val FORTUNES_QUERY = "SELECT id, message FROM fortune" +const val DB_ROWS = 10_000 + +@OptIn(ExperimentalCoroutinesApi::class) +fun Application.main() { + val poolSize = Runtime.getRuntime().availableProcessors() * 2 + val pool = HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) }) + + // Create a dedicated dispatcher for database operations + val databaseDispatcher = Dispatchers.IO.limitedParallelism(poolSize) + val helloWorldContent = TextContent(HELLO_WORLD, ContentType.Text.Plain) + val random = Random.Default + + install(DefaultHeaders) + + routing { + get("/plaintext") { + call.respond(helloWorldContent) + } + + get("/json") { + call.respondJson(Message(HELLO_WORLD)) + } + + get("/db") { + val world = withContext(databaseDispatcher) { + pool.connection.use { connection -> + connection.prepareStatement(WORLD_QUERY).use { statement -> + statement.setInt(1, random.nextInt(DB_ROWS) + 1) + statement.executeQuery().use { rs -> + rs.next() + World(rs.getInt(1), rs.getInt(2)) + } + } + } + } + call.respondJson(world) + } + + fun Connection.selectWorlds(queries: Int): Array = + prepareStatement(WORLD_QUERY).use { statement -> + Array(queries) { i -> + statement.setInt(1, random.nextInt(DB_ROWS) + 1) + statement.executeQuery().use { rs -> + rs.next() + World(rs.getInt(1), rs.getInt(2)) + } + } + } + + get("/queries") { + val queries = call.queries() + val result = withContext(databaseDispatcher) { + pool.connection.use { it.selectWorlds(queries) } + } + call.respondJson(result) + } + + get("/fortunes") { + val result = mutableListOf() + withContext(databaseDispatcher) { + pool.connection.use { connection -> + connection.prepareStatement(FORTUNES_QUERY).use { statement -> + statement.executeQuery().use { rs -> + while (rs.next()) { + result += Fortune(rs.getInt(1), rs.getString(2)) + } + } + } + } + } + result.add(Fortune(0, "Additional fortune added at request time.")) + result.sortBy { it.message } + call.respondHtml { + head { title { +"Fortunes" } } + body { + table { + tr { + th { +"id" } + th { +"message" } + } + for (fortune in result) { + tr { + td { +fortune.id.toString() } + td { +fortune.message } + } + } + } + } + } + } + + get("/updates") { + val queries = call.queries() + val result: Array + + withContext(databaseDispatcher) { + pool.connection.use { connection -> + result = connection.selectWorlds(queries) + + val updateSql = StringJoiner( + ", ", + "UPDATE World SET randomNumber = temp.randomNumber FROM (VALUES ", + " ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = World.id" + ) + + for (i in result.indices) { + result[i].randomNumber = random.nextInt(DB_ROWS) + 1 + updateSql.add("(?, ?)") + } + + connection.prepareStatement(updateSql.toString()).use { statement -> + var paramIndex = 0 + for (world in result) { + statement.setInt(++paramIndex, world.id) + statement.setInt(++paramIndex, world.randomNumber) + } + statement.executeUpdate() + } + } + } + + call.respondJson(result) + } + } +} + + +fun ApplicationCall.queries() = + request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1 diff --git a/frameworks/Kotlin/ktor/ktor/src/main/resources/application.conf b/frameworks/Kotlin/ktor/ktor/src/main/resources/application.conf index 8faba62906a..ad26f4f1ddc 100644 --- a/frameworks/Kotlin/ktor/ktor/src/main/resources/application.conf +++ b/frameworks/Kotlin/ktor/ktor/src/main/resources/application.conf @@ -7,6 +7,6 @@ ktor { } application { - modules = [ org.jetbrains.ktor.benchmarks.HelloKt.main ] + modules = [org.jetbrains.ktor.benchmarks.MainKt.main] } }