diff --git a/frameworks/Kotlin/ktor/benchmark_config.json b/frameworks/Kotlin/ktor/benchmark_config.json index 17373df20ee..fa240513896 100644 --- a/frameworks/Kotlin/ktor/benchmark_config.json +++ b/frameworks/Kotlin/ktor/benchmark_config.json @@ -140,7 +140,7 @@ "notes": "http://ktor.io/", "versus": "netty" }, - "exposed-dsl": { + "exposed-jdbc-dsl": { "db_url": "/db", "query_url": "/queries?queries=", "update_url": "/updates?queries=", @@ -157,11 +157,11 @@ "webserver": "None", "os": "Linux", "database_os": "Linux", - "display_name": "ktor-netty-exposed-dsl", + "display_name": "ktor-netty-exposed-jdbc-dsl", "notes": "", "versus": "ktor" }, - "exposed-dao": { + "exposed-jdbc-dao": { "db_url": "/db", "fortune_url": "/fortunes", "port": 9090, @@ -176,7 +176,28 @@ "webserver": "None", "os": "Linux", "database_os": "Linux", - "display_name": "ktor-netty-exposed-dao", + "display_name": "ktor-netty-exposed-jdbc-dao", + "notes": "", + "versus": "ktor" + }, + "exposed-r2dbc-dsl": { + "db_url": "/db", + "query_url": "/queries?queries=", + "update_url": "/updates?queries=", + "fortune_url": "/fortunes", + "port": 9090, + "approach": "Realistic", + "classification": "Micro", + "database": "postgres", + "framework": "ktor", + "language": "Kotlin", + "flavor": "None", + "orm": "Full", + "platform": "Netty", + "webserver": "None", + "os": "Linux", + "database_os": "Linux", + "display_name": "ktor-netty-exposed-r2dbc-dsl", "notes": "", "versus": "ktor" } diff --git a/frameworks/Kotlin/ktor/ktor-asyncdb/README.md b/frameworks/Kotlin/ktor/ktor-asyncdb/README.md index 6b4beb607cf..efc38e7b225 100755 --- a/frameworks/Kotlin/ktor/ktor-asyncdb/README.md +++ b/frameworks/Kotlin/ktor/ktor-asyncdb/README.md @@ -8,24 +8,24 @@ This sets up testing using [Ktor](https://ktor.io/), with a couple of async Post ## Test URLs ### JSON -http://localhost:8080/json +http://localhost:9090/json ### PLAINTEXT -http://localhost:8080/plaintext +http://localhost:9090/plaintext ### DB -http://localhost:8080/db +http://localhost:9090/db ### QUERY -http://localhost:8080/query?queries= +http://localhost:9090/query?queries= ### UPDATE -http://localhost:8080/update?queries= +http://localhost:9090/update?queries= ### FORTUNES -http://localhost:8080/fortunes \ No newline at end of file +http://localhost:9090/fortunes \ No newline at end of file diff --git a/frameworks/Kotlin/ktor/ktor-exposed-dsl.dockerfile b/frameworks/Kotlin/ktor/ktor-exposed-dsl.dockerfile deleted file mode 100644 index d89f899a9d1..00000000000 --- a/frameworks/Kotlin/ktor/ktor-exposed-dsl.dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM gradle:jdk21 - -WORKDIR /ktor-exposed -COPY ktor-exposed/settings.gradle.kts settings.gradle.kts -COPY ktor-exposed/app app -RUN gradle --no-daemon shadowJar - -EXPOSE 8080 - -CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseG1GC", "-XX:+AlwaysPreTouch", "-jar", "app/build/libs/app-all.jar", "Dsl"] diff --git a/frameworks/Kotlin/ktor/ktor-exposed-dao.dockerfile b/frameworks/Kotlin/ktor/ktor-exposed-jdbc-dao.dockerfile similarity index 66% rename from frameworks/Kotlin/ktor/ktor-exposed-dao.dockerfile rename to frameworks/Kotlin/ktor/ktor-exposed-jdbc-dao.dockerfile index 417c0358735..1164bfd4048 100644 --- a/frameworks/Kotlin/ktor/ktor-exposed-dao.dockerfile +++ b/frameworks/Kotlin/ktor/ktor-exposed-jdbc-dao.dockerfile @@ -1,10 +1,10 @@ -FROM gradle:jdk21 +FROM gradle:jdk25 WORKDIR /ktor-exposed COPY ktor-exposed/settings.gradle.kts settings.gradle.kts COPY ktor-exposed/app app RUN gradle --no-daemon shadowJar -EXPOSE 8080 +EXPOSE 9090 -CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app/build/libs/app-all.jar", "Dao"] +CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app/build/libs/app-all.jar", "Jdbc", "Dao"] diff --git a/frameworks/Kotlin/ktor/ktor-exposed-jdbc-dsl.dockerfile b/frameworks/Kotlin/ktor/ktor-exposed-jdbc-dsl.dockerfile new file mode 100644 index 00000000000..01182459fdc --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-exposed-jdbc-dsl.dockerfile @@ -0,0 +1,10 @@ +FROM gradle:jdk25 + +WORKDIR /ktor-exposed +COPY ktor-exposed/settings.gradle.kts settings.gradle.kts +COPY ktor-exposed/app app +RUN gradle --no-daemon shadowJar + +EXPOSE 9090 + +CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app/build/libs/app-all.jar", "Jdbc", "Dsl"] diff --git a/frameworks/Kotlin/ktor/ktor-exposed-r2dbc-dsl.dockerfile b/frameworks/Kotlin/ktor/ktor-exposed-r2dbc-dsl.dockerfile new file mode 100644 index 00000000000..53698f92c47 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-exposed-r2dbc-dsl.dockerfile @@ -0,0 +1,10 @@ +FROM gradle:jdk25 + +WORKDIR /ktor-exposed +COPY ktor-exposed/settings.gradle.kts settings.gradle.kts +COPY ktor-exposed/app app +RUN gradle --no-daemon shadowJar + +EXPOSE 9090 + +CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app/build/libs/app-all.jar", "R2dbc", "Dsl"] diff --git a/frameworks/Kotlin/ktor/ktor-exposed/README.md b/frameworks/Kotlin/ktor/ktor-exposed/README.md index 423c8ddecd2..b4e38280b5f 100644 --- a/frameworks/Kotlin/ktor/ktor-exposed/README.md +++ b/frameworks/Kotlin/ktor/ktor-exposed/README.md @@ -15,16 +15,16 @@ The tests were run with: ## Test URLs ### DB -http://localhost:8080/db +http://localhost:9090/db ### QUERY -http://localhost:8080/query?queries= +http://localhost:9090/query?queries= ### UPDATE -http://localhost:8080/update?queries= +http://localhost:9090/update?queries= ### FORTUNES -http://localhost:8080/fortunes +http://localhost:9090/fortunes diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts b/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts index 5f29b997834..58e5e52f4d2 100644 --- a/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/build.gradle.kts @@ -3,9 +3,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { application - kotlin("jvm") version "2.1.21" - kotlin("plugin.serialization") version "2.1.21" - id("com.gradleup.shadow") version "8.3.9" + kotlin("jvm") version "2.3.0" + kotlin("plugin.serialization") version "2.3.0" + id("com.gradleup.shadow") version "9.3.0" } repositories { @@ -13,8 +13,8 @@ repositories { } val ktorVersion = "3.3.3" -val kotlinxSerializationVersion = "1.8.1" -val exposedVersion = "0.61.0" +val kotlinxSerializationVersion = "1.9.0" +val exposedVersion = "1.0.0-rc-4" dependencies { implementation("io.ktor:ktor-server-core:$ktorVersion") @@ -27,20 +27,17 @@ dependencies { implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-r2dbc:${exposedVersion}") - implementation("org.postgresql:postgresql:42.7.5") - implementation("com.zaxxer:HikariCP:5.1.0") - runtimeOnly("org.slf4j:slf4j-simple:1.7.36") -} + implementation("org.postgresql:postgresql:42.7.8") + implementation("com.zaxxer:HikariCP:7.0.2") -application.mainClass.set("AppKt") + implementation("org.postgresql:r2dbc-postgresql:1.1.1.RELEASE") + implementation("io.r2dbc:r2dbc-pool:1.0.2.RELEASE") -kotlin { - jvmToolchain(21) + runtimeOnly("org.slf4j:slf4j-simple:2.0.7") } -tasks.withType().configureEach { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } -} +application.mainClass.set("AppKt") + +kotlin.jvmToolchain(25) 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 4b4bf93741d..50f530b52ac 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 @@ -1,6 +1,8 @@ -import ExposedMode.* +import ExposedMode.Dao +import ExposedMode.Dsl import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource +import database.* import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.engine.* @@ -9,137 +11,211 @@ import io.ktor.server.netty.* import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.response.* import io.ktor.server.routing.* -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.toList import kotlinx.html.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import org.jetbrains.exposed.dao.IntEntity -import org.jetbrains.exposed.dao.IntEntityClass -import org.jetbrains.exposed.dao.id.EntityID -import org.jetbrains.exposed.dao.id.IdTable -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.Transaction -import org.jetbrains.exposed.sql.statements.BatchUpdateStatement -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement +import org.jetbrains.exposed.v1.core.vendors.PostgreSQLDialect +import org.jetbrains.exposed.v1.jdbc.Database +import org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction +import org.jetbrains.exposed.v1.r2dbc.R2dbcDatabase +import org.jetbrains.exposed.v1.r2dbc.R2dbcDatabaseConfig +import org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction import java.util.concurrent.ThreadLocalRandom +import org.jetbrains.exposed.v1.jdbc.select as jdbcSelect +import org.jetbrains.exposed.v1.jdbc.statements.toExecutable as toJdbcExecutable +import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager.Companion as JdbcTransactionManager +import org.jetbrains.exposed.v1.r2dbc.select as r2dbcSelect +import org.jetbrains.exposed.v1.r2dbc.statements.toExecutable as toR2dbcExecutable +import org.jetbrains.exposed.v1.r2dbc.transactions.TransactionManager as R2dbcTransactionManager + +enum class ConnectionMode { + Jdbc, R2dbc +} -@Serializable -data class World(val id: Int, var randomNumber: Int) - -@Serializable -data class Fortune(val id: Int, var message: String) +enum class ExposedMode { + Dsl, Dao +} +fun main(args: Array) { + val connectionMode = ConnectionMode.valueOf(args[0]) + val exposedMode = ExposedMode.valueOf(args[1]) + embeddedServer(Netty, port = 9090) { module(connectionMode, exposedMode) }.start(wait = true) +} -// see "toolset/databases/postgres/create-postgres.sql" -object WorldTable : IdTable("World") { - override val id = integer("id").entityId() - val randomNumber = integer("randomnumber").default(0) // The name is "randomNumber" in "create-postgres.sql". -} +fun Application.module(connectionMode: ConnectionMode, exposedMode: ExposedMode) = + parameterizedModule( + when (connectionMode) { + ConnectionMode.Jdbc -> when (exposedMode) { + Dsl -> ExposedOps.Jdbc.Dsl + Dao -> ExposedOps.Jdbc.Dao + } -object FortuneTable : IdTable("Fortune") { - override val id = integer("id").entityId() - val message = varchar("message", 2048) -} + ConnectionMode.R2dbc -> when (exposedMode) { + Dsl -> ExposedOps.R2dbc.Dsl + Dao -> throw IllegalArgumentException("DAO with R2DBC is not supported") + } + } + ) +fun ApplicationCall.queries() = + request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1 -class WorldDao(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(WorldTable) +private const val DB_ROWS = 10_000 +fun ThreadLocalRandom.nextIntWithinRows() = + nextInt(DB_ROWS) + 1 + +interface ExposedOps { + fun createDatabase(): TDatabase + suspend fun transaction(db: TDatabase, statement: suspend /*JdbcTransaction.*/() -> T): T + + // Repository pattern functions. These can also be extracted into a separate interface. + suspend fun getWorldWithId(id: Int): World + suspend fun getRandomWorlds(queries: Int, random: ThreadLocalRandom): List + suspend fun getAllFortunesAndAddTo(result: MutableList) + suspend fun getRandomWorldsAndUpdate(queries: Int, random: ThreadLocalRandom): List + + interface Jdbc : ExposedOps { + override fun createDatabase(): Database { + val poolSize = Runtime.getRuntime().availableProcessors() * 2 + val pool = HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) }) + return Database.connect(pool) + } - var randomNumber by WorldTable.randomNumber - fun toWorld() = - World(id.value, randomNumber) -} + override suspend fun transaction(db: Database, statement: suspend () -> T): T = + suspendTransaction(db) { statement() } -class FortuneDao(id: EntityID) : IntEntity(id) { - companion object : IntEntityClass(FortuneTable) + object Dsl : Jdbc { + override suspend fun getWorldWithId(id: Int): World = + WorldTable.jdbcSelect(WorldTable.id, WorldTable.randomNumber).where(WorldTable.id eq id) + .single().toWorld() - var message by FortuneTable.message - fun toFortune() = - Fortune(id.value, message) -} + override suspend fun getRandomWorlds(queries: Int, random: ThreadLocalRandom): List = + // Coroutine concurrent `select`s lead to connection pool exhaustion. + List(queries) { getWorldWithId(random.nextIntWithinRows()) } + override suspend fun getAllFortunesAndAddTo(result: MutableList) { + FortuneTable.jdbcSelect(FortuneTable.id, FortuneTable.message) + .mapTo(result) { it.toFortune() } + } -enum class ExposedMode { - Dsl, Dao -} + override suspend fun getRandomWorldsAndUpdate(queries: Int, random: ThreadLocalRandom): List { + val result = getRandomWorlds(queries, random) + result.forEach { it.randomNumber = random.nextIntWithinRows() } + val batch = BatchUpdateStatement(WorldTable) + result.sortedBy { it.id }.forEach { world -> + batch.addBatch(EntityID(world.id, WorldTable)) + batch[WorldTable.randomNumber] = world.randomNumber + } + // also consider passing the transaction explicitly + batch.toJdbcExecutable().execute(JdbcTransactionManager.current()) + return result + } + } -fun main(args: Array) { - val exposedMode = valueOf(args.first()) - embeddedServer(Netty, port = 9090) { module(exposedMode) }.start(wait = true) -} + object Dao : Jdbc { + override suspend fun getWorldWithId(id: Int): World = + WorldDao[id].toWorld() -fun Application.module(exposedMode: ExposedMode) { - val poolSize = Runtime.getRuntime().availableProcessors() * 2 - val pool = HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) }) - val database = Database.connect(pool) - val databaseDispatcher = Dispatchers.IO.limitedParallelism(poolSize) - suspend fun withDatabaseTransaction(statement: suspend Transaction.() -> T) = - newSuspendedTransaction(context = databaseDispatcher, db = database, statement = statement) + override suspend fun getRandomWorlds(queries: Int, random: ThreadLocalRandom): List = + //List(queries) { WorldDao[random.nextIntWithinRows()].toWorld() } + throw IllegalArgumentException("DAO not supported because it appears to cache results") - install(DefaultHeaders) + override suspend fun getAllFortunesAndAddTo(result: MutableList) { + FortuneDao.all().mapTo(result) { it.toFortune() } + } - routing { - fun selectWorldsWithIdQuery(id: Int) = - WorldTable.select(WorldTable.id, WorldTable.randomNumber).where(WorldTable.id eq id) + override suspend fun getRandomWorldsAndUpdate(queries: Int, random: ThreadLocalRandom): List { + /* + val worldDaosAndNewRandomNumbers = + List(queries) { WorldDao[random.nextIntWithinRows()] to random.nextIntWithinRows() } + worldDaosAndNewRandomNumbers + .sortedBy { (worldDao, _) -> worldDao.id.value } + .forEach { (worldDao, newRandomNumber) -> + worldDao.randomNumber = newRandomNumber + } + val result = worldDaosAndNewRandomNumbers.map { (worldDao, _) -> worldDao.toWorld() } + */ + throw IllegalArgumentException("DAO not supported because it appears to cache results") + } + } + } - fun ResultRow.toWorld() = - World(this[WorldTable.id].value, this[WorldTable.randomNumber]) + // TODO consider moving to separate files to avoid import conflicts/aliases + interface R2dbc : ExposedOps { + override fun createDatabase(): R2dbcDatabase = + R2dbcDatabase.connect(configurePostgresR2DBC(), R2dbcDatabaseConfig { + // This can't be omitted. + explicitDialect = PostgreSQLDialect() + }) + + override suspend fun transaction(db: R2dbcDatabase, statement: suspend () -> T): T = + suspendTransaction(db) { statement() } + + object Dsl : R2dbc { + override suspend fun getWorldWithId(id: Int): World = + WorldTable.r2dbcSelect(WorldTable.id, WorldTable.randomNumber).where(WorldTable.id eq id) + .single().toWorld() + + override suspend fun getRandomWorlds(queries: Int, random: ThreadLocalRandom): List = + // Coroutine concurrent `select`s lead to connection pool exhaustion. + List(queries) { getWorldWithId(random.nextIntWithinRows()) } + + override suspend fun getAllFortunesAndAddTo(result: MutableList) { + FortuneTable.r2dbcSelect(FortuneTable.id, FortuneTable.message) + .map { it.toFortune() }.toList(result) + } - fun ResultRow.toFortune() = - Fortune(this[FortuneTable.id].value, this[FortuneTable.message]) + override suspend fun getRandomWorldsAndUpdate(queries: Int, random: ThreadLocalRandom): List { + val result = getRandomWorlds(queries, random) + result.forEach { it.randomNumber = random.nextIntWithinRows() } + val batch = BatchUpdateStatement(WorldTable) + result.sortedBy { it.id }.forEach { world -> + batch.addBatch(EntityID(world.id, WorldTable)) + batch[WorldTable.randomNumber] = world.randomNumber + } + // also consider passing the transaction explicitly + batch.toR2dbcExecutable().execute(R2dbcTransactionManager.current()) + return result + } + } + } +} - fun ThreadLocalRandom.nextIntWithinRows() = - nextInt(DB_ROWS) + 1 +fun Application.parameterizedModule(exposedOps: ExposedOps) { + install(DefaultHeaders) - fun selectSingleWorld(random: ThreadLocalRandom): World = - selectWorldsWithIdQuery(random.nextIntWithinRows()).single().toWorld() + routing { + val database = exposedOps.createDatabase() - fun selectWorlds(queries: Int, random: ThreadLocalRandom): List = - List(queries) { selectSingleWorld(random) } + suspend fun transaction(statement: suspend () -> T): T = + exposedOps.transaction(database) { statement() } get("/db") { val random = ThreadLocalRandom.current() - val result = withDatabaseTransaction { - when (exposedMode) { - Dsl -> selectSingleWorld(random) - Dao -> WorldDao[random.nextIntWithinRows()].toWorld() - } - } - call.respondText(Json.encodeToString(result), ContentType.Application.Json) + val result = transaction { exposedOps.getWorldWithId(random.nextIntWithinRows()) } + call.respondText(json.encodeToString(result), ContentType.Application.Json) } - get("/queries") { val queries = call.queries() val random = ThreadLocalRandom.current() - - val result = withDatabaseTransaction { - when (exposedMode) { - Dsl -> selectWorlds(queries, random) - Dao -> //List(queries) { WorldDao[random.nextIntWithinRows()].toWorld() } - throw IllegalArgumentException("DAO not supported because it appears to cache results") - } - } - - call.respondText(Json.encodeToString(result), ContentType.Application.Json) + val result = transaction { exposedOps.getRandomWorlds(queries, random) } + call.respondText(json.encodeToString(result), ContentType.Application.Json) } get("/fortunes") { - val result = withDatabaseTransaction { - when (exposedMode) { - Dsl -> FortuneTable.select(FortuneTable.id, FortuneTable.message) - .asSequence().map { it.toFortune() } + val result = mutableListOf() - Dao -> FortuneDao.all().asSequence().map { it.toFortune() } - }.toMutableList() - } + transaction { exposedOps.getAllFortunesAndAddTo(result) } result.add(Fortune(0, "Additional fortune added at request time.")) result.sortBy { it.message } + call.respondHtml { head { title { +"Fortunes" } } body { @@ -162,62 +238,8 @@ fun Application.module(exposedMode: ExposedMode) { get("/updates") { val queries = call.queries() val random = ThreadLocalRandom.current() - lateinit var result: List - - withDatabaseTransaction { - when (exposedMode) { - Dsl -> { - result = selectWorlds(queries, random) - result.forEach { it.randomNumber = random.nextIntWithinRows() } - val batch = BatchUpdateStatement(WorldTable) - result.sortedBy { it.id }.forEach { world -> - batch.addBatch(EntityID(world.id, WorldTable)) - batch[WorldTable.randomNumber] = world.randomNumber - } - batch.execute(TransactionManager.current()) - } - - Dao -> /*{ - val worldDaosAndNewRandomNumbers = - List(queries) { WorldDao[random.nextIntWithinRows()] to random.nextIntWithinRows() } - worldDaosAndNewRandomNumbers - .sortedBy { (worldDao, _) -> worldDao.id.value } - .forEach { (worldDao, newRandomNumber) -> - worldDao.randomNumber = newRandomNumber - } - result = worldDaosAndNewRandomNumbers.map { (worldDao, _) -> worldDao.toWorld() } - }*/ - throw IllegalArgumentException("DAO not supported because it appears to cache results") - } - } - - call.respondText(Json.encodeToString(result), ContentType.Application.Json) + val result = transaction { exposedOps.getRandomWorldsAndUpdate(queries, random) } + call.respondText(json.encodeToString(result), ContentType.Application.Json) } } } - -private const val DB_ROWS = 10_000 - -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 = 10000 - maximumPoolSize = poolSize - minimumIdle = poolSize -} - -fun ApplicationCall.queries() = - request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1 - diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/Json.kt b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/Json.kt new file mode 100644 index 00000000000..53f3bf19c06 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/Json.kt @@ -0,0 +1,10 @@ +import kotlinx.serialization.json.Json + +// copied from the `ktor` portion +// Optimized JSON instance with better performance settings +internal val json = Json { + prettyPrint = false + isLenient = true + ignoreUnknownKeys = true + coerceInputValues = true +} diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/Models.kt b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/Models.kt new file mode 100644 index 00000000000..9791ee4a7fa --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/Models.kt @@ -0,0 +1,7 @@ +import kotlinx.serialization.Serializable + +@Serializable +data class World(val id: Int, var randomNumber: Int) + +@Serializable +data class Fortune(val id: Int, var message: String) diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/Exposed.kt b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/Exposed.kt new file mode 100644 index 00000000000..ef400c54b03 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/Exposed.kt @@ -0,0 +1,45 @@ +package database + +import Fortune +import World +import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +// see "toolset/databases/postgres/create-postgres.sql" + +object WorldTable : IdTable("World") { + override val id = integer("id").entityId() + val randomNumber = integer("randomnumber").default(0) // The name is "randomNumber" in "create-postgres.sql". +} + +object FortuneTable : IdTable("Fortune") { + override val id = integer("id").entityId() + val message = varchar("message", 2048) +} + + +fun ResultRow.toWorld() = + World(this[WorldTable.id].value, this[WorldTable.randomNumber]) + +fun ResultRow.toFortune() = + Fortune(this[FortuneTable.id].value, this[FortuneTable.message]) + + +class WorldDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(WorldTable) + + var randomNumber by WorldTable.randomNumber + fun toWorld() = + World(id.value, randomNumber) +} + +class FortuneDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(FortuneTable) + + var message by FortuneTable.message + fun toFortune() = + Fortune(id.value, message) +} diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/Jdbc.kt b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/Jdbc.kt new file mode 100644 index 00000000000..6ab4fa6e6b9 --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/Jdbc.kt @@ -0,0 +1,28 @@ +package database + +import com.zaxxer.hikari.HikariConfig + +// copied from the `ktor` portion + +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 +} diff --git a/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/R2dbc.kt b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/R2dbc.kt new file mode 100644 index 00000000000..63b80ccaafa --- /dev/null +++ b/frameworks/Kotlin/ktor/ktor-exposed/app/src/main/kotlin/database/R2dbc.kt @@ -0,0 +1,37 @@ +package database + +import io.r2dbc.pool.ConnectionPool +import io.r2dbc.pool.ConnectionPoolConfiguration +import io.r2dbc.postgresql.PostgresqlConnectionConfiguration +import io.r2dbc.postgresql.PostgresqlConnectionFactory +import io.r2dbc.postgresql.client.SSLMode +import io.r2dbc.spi.ConnectionFactory +import java.time.Duration + +// copied and adapted from the `ktor-r2dbc` portion + +fun configurePostgresR2DBC(): ConnectionFactory { + val cfo = PostgresqlConnectionConfiguration.builder() + .host("tfb-database") + .port(5432) + .database("hello_world") + .username("benchmarkdbuser") + .password("benchmarkdbpass") + //.loopResources { NioClientEventLoopResources(Runtime.getRuntime().availableProcessors()).cacheLoops() } + .sslMode(SSLMode.DISABLE) + .tcpKeepAlive(true) + .tcpNoDelay(true) + .build() + + val cf = PostgresqlConnectionFactory(cfo) + + val cp = ConnectionPoolConfiguration.builder(cf) + .initialSize(512) + .maxSize(512) + .maxIdleTime(Duration.ofSeconds(30)) + .maxAcquireTime(Duration.ofSeconds(5)) + .validationQuery("SELECT 1") + .build() + + return ConnectionPool(cp) +} diff --git a/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.jar b/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.jar index ccebba7710d..f8e1ee3125f 100644 Binary files a/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.jar and b/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.jar differ diff --git a/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.properties b/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.properties index 18362b78bde..23449a2b543 100644 --- a/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.properties +++ b/frameworks/Kotlin/ktor/ktor-exposed/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/frameworks/Kotlin/ktor/ktor-exposed/gradlew b/frameworks/Kotlin/ktor/ktor-exposed/gradlew index 79a61d421cc..adff685a034 100755 --- a/frameworks/Kotlin/ktor/ktor-exposed/gradlew +++ b/frameworks/Kotlin/ktor/ktor-exposed/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -133,10 +132,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -169,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -197,16 +198,19 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/frameworks/Kotlin/ktor/ktor-exposed/gradlew.bat b/frameworks/Kotlin/ktor/ktor-exposed/gradlew.bat index 93e3f59f135..c4bdd3ab8e3 100644 --- a/frameworks/Kotlin/ktor/ktor-exposed/gradlew.bat +++ b/frameworks/Kotlin/ktor/ktor-exposed/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/frameworks/Kotlin/ktor/ktor-r2dbc.dockerfile b/frameworks/Kotlin/ktor/ktor-r2dbc.dockerfile index 9aa09916483..b44dba829d6 100644 --- a/frameworks/Kotlin/ktor/ktor-r2dbc.dockerfile +++ b/frameworks/Kotlin/ktor/ktor-r2dbc.dockerfile @@ -9,4 +9,4 @@ COPY --from=build /ktor-r2dbc/build/libs/tech-empower-framework-benchmark-1.0-SN EXPOSE 9090 -CMD ["java", "-server","-XX:+UseNUMA", "-XX:+UseG1GC", "-XX:+AlwaysPreTouch", "-jar", "app.jar"] +CMD ["java", "-server","-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app.jar"] diff --git a/frameworks/Kotlin/ktor/ktor.dockerfile b/frameworks/Kotlin/ktor/ktor.dockerfile index d0d81f0217f..ac54a7e9bc6 100644 --- a/frameworks/Kotlin/ktor/ktor.dockerfile +++ b/frameworks/Kotlin/ktor/ktor.dockerfile @@ -9,4 +9,4 @@ COPY --from=build /ktor/build/libs/tech-empower-framework-benchmark-1.0-SNAPSHOT EXPOSE 9090 -CMD ["java", "-server","-XX:+UseNUMA", "-XX:+UseG1GC", "-XX:+AlwaysPreTouch", "-jar", "app.jar"] +CMD ["java", "-server","-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app.jar"]