From e349bd171fcbda43e142e099392a5d9a6680dcb4 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:01:05 +0000 Subject: [PATCH 1/8] =?UTF-8?q?Add=20Ktor:=20JetBrains=20Kotlin=20web=20fr?= =?UTF-8?q?amework=20on=20Netty=20(~14k=20=E2=AD=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frameworks/ktor/Dockerfile | 19 ++ frameworks/ktor/build.gradle.kts | 37 +++ frameworks/ktor/gradle.properties | 1 + frameworks/ktor/meta.json | 19 ++ frameworks/ktor/settings.gradle.kts | 1 + .../main/kotlin/com/httparena/Application.kt | 285 ++++++++++++++++++ .../ktor/src/main/resources/logback.xml | 10 + 7 files changed, 372 insertions(+) create mode 100644 frameworks/ktor/Dockerfile create mode 100644 frameworks/ktor/build.gradle.kts create mode 100644 frameworks/ktor/gradle.properties create mode 100644 frameworks/ktor/meta.json create mode 100644 frameworks/ktor/settings.gradle.kts create mode 100644 frameworks/ktor/src/main/kotlin/com/httparena/Application.kt create mode 100644 frameworks/ktor/src/main/resources/logback.xml diff --git a/frameworks/ktor/Dockerfile b/frameworks/ktor/Dockerfile new file mode 100644 index 0000000..536eee0 --- /dev/null +++ b/frameworks/ktor/Dockerfile @@ -0,0 +1,19 @@ +FROM gradle:8.12-jdk21 AS build +WORKDIR /app +COPY build.gradle.kts settings.gradle.kts gradle.properties ./ +RUN gradle dependencies --no-daemon -q 2>/dev/null || true +COPY src ./src +RUN gradle buildFatJar --no-daemon -q + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/build/libs/ktor-httparena.jar . +EXPOSE 8080 +ENTRYPOINT ["java", \ + "-server", \ + "-XX:+UseParallelGC", \ + "-XX:+UseNUMA", \ + "-XX:-StackTraceInThrowable", \ + "-Dio.netty.buffer.checkBounds=false", \ + "-Dio.netty.buffer.checkAccessible=false", \ + "-jar", "ktor-httparena.jar"] diff --git a/frameworks/ktor/build.gradle.kts b/frameworks/ktor/build.gradle.kts new file mode 100644 index 0000000..617ebe2 --- /dev/null +++ b/frameworks/ktor/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + kotlin("jvm") version "2.1.10" + kotlin("plugin.serialization") version "2.1.10" + id("io.ktor.plugin") version "3.1.1" + application +} + +group = "com.httparena" +version = "1.0.0" + +application { + mainClass.set("com.httparena.ApplicationKt") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.ktor:ktor-server-core:3.1.1") + implementation("io.ktor:ktor-server-netty:3.1.1") + implementation("io.ktor:ktor-server-compression:3.1.1") + implementation("io.ktor:ktor-server-default-headers:3.1.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") + implementation("org.xerial:sqlite-jdbc:3.47.2.0") + implementation("ch.qos.logback:logback-classic:1.5.15") +} + +ktor { + fatJar { + archiveFileName.set("ktor-httparena.jar") + } +} + +kotlin { + jvmToolchain(21) +} diff --git a/frameworks/ktor/gradle.properties b/frameworks/ktor/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/frameworks/ktor/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/frameworks/ktor/meta.json b/frameworks/ktor/meta.json new file mode 100644 index 0000000..71d7e7a --- /dev/null +++ b/frameworks/ktor/meta.json @@ -0,0 +1,19 @@ +{ + "display_name": "ktor", + "language": "Kotlin", + "type": "framework", + "engine": "Netty", + "description": "JetBrains Ktor 3.x on Netty with Kotlin coroutines, kotlinx.serialization, JDK 21.", + "repo": "https://github.com/ktorio/ktor", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "noisy", + "mixed" + ] +} diff --git a/frameworks/ktor/settings.gradle.kts b/frameworks/ktor/settings.gradle.kts new file mode 100644 index 0000000..0070044 --- /dev/null +++ b/frameworks/ktor/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "ktor-httparena" diff --git a/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt new file mode 100644 index 0000000..d5b8a51 --- /dev/null +++ b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt @@ -0,0 +1,285 @@ +package com.httparena + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.compression.* +import io.ktor.server.plugins.defaultheaders.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.io.ByteArrayOutputStream +import java.io.File +import java.sql.Connection +import java.sql.DriverManager +import java.util.zip.GZIPOutputStream + +@Serializable +data class DatasetItem( + val id: Int, + val name: String, + val category: String, + val price: Double, + val quantity: Int, + val active: Boolean, + val tags: List, + val rating: RatingInfo +) + +@Serializable +data class RatingInfo( + val score: Double, + val count: Int +) + +@Serializable +data class ProcessedItem( + val id: Int, + val name: String, + val category: String, + val price: Double, + val quantity: Int, + val active: Boolean, + val tags: List, + val rating: RatingInfo, + val total: Double +) + +@Serializable +data class JsonResponse( + val items: List, + val count: Int +) + +@Serializable +data class DbItem( + val id: Int, + val name: String, + val category: String, + val price: Double, + val quantity: Int, + val active: Boolean, + val tags: List, + val rating: RatingInfo +) + +@Serializable +data class DbResponse( + val items: List, + val count: Int +) + +object AppData { + val json = Json { ignoreUnknownKeys = true } + var dataset: List = emptyList() + var jsonCache: ByteArray = ByteArray(0) + var largeJsonCache: ByteArray = ByteArray(0) + var largeGzipCache: ByteArray = ByteArray(0) + val staticFiles: MutableMap> = mutableMapOf() + var db: Connection? = null + + private val mimeTypes = mapOf( + ".css" to "text/css", + ".js" to "application/javascript", + ".html" to "text/html", + ".woff2" to "font/woff2", + ".svg" to "image/svg+xml", + ".webp" to "image/webp", + ".json" to "application/json" + ) + + fun load() { + // Dataset + val path = System.getenv("DATASET_PATH") ?: "/data/dataset.json" + val dataFile = File(path) + if (dataFile.exists()) { + dataset = json.decodeFromString>(dataFile.readText()) + jsonCache = buildJsonCache(dataset) + } + + // Large dataset for compression + val largeFile = File("/data/dataset-large.json") + if (largeFile.exists()) { + val largeItems = json.decodeFromString>(largeFile.readText()) + largeJsonCache = buildJsonCache(largeItems) + // Pre-compress for gzip + largeGzipCache = gzipCompress(largeJsonCache) + } + + // Static files + val staticDir = File("/data/static") + if (staticDir.isDirectory) { + staticDir.listFiles()?.forEach { file -> + if (file.isFile) { + val ext = file.extension.let { if (it.isNotEmpty()) ".$it" else "" } + val ct = mimeTypes[ext] ?: "application/octet-stream" + staticFiles[file.name] = file.readBytes() to ct + } + } + } + + // Database + val dbFile = File("/data/benchmark.db") + if (dbFile.exists()) { + db = DriverManager.getConnection("jdbc:sqlite:file:/data/benchmark.db?mode=ro&immutable=1") + db!!.createStatement().execute("PRAGMA mmap_size=268435456") + } + } + + private fun buildJsonCache(items: List): ByteArray { + val processed = items.map { d -> + ProcessedItem( + id = d.id, name = d.name, category = d.category, + price = d.price, quantity = d.quantity, active = d.active, + tags = d.tags, rating = d.rating, + total = Math.round(d.price * d.quantity * 100.0) / 100.0 + ) + } + val resp = JsonResponse(items = processed, count = processed.size) + return json.encodeToString(JsonResponse.serializer(), resp).toByteArray() + } + + private fun gzipCompress(data: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(data.size / 4) + GZIPOutputStream(bos).use { it.write(data) } + return bos.toByteArray() + } +} + +fun main() { + AppData.load() + println("Ktor HttpArena server starting on :8080") + + embeddedServer(Netty, port = 8080, host = "0.0.0.0") { + install(DefaultHeaders) { + header("Server", "ktor") + } + + routing { + get("/pipeline") { + call.respondText("ok", ContentType.Text.Plain) + } + + get("/baseline11") { + val sum = sumQueryParams(call) + call.respondText(sum.toString(), ContentType.Text.Plain) + } + + post("/baseline11") { + var sum = sumQueryParams(call) + val body = call.receiveText().trim() + body.toLongOrNull()?.let { sum += it } + call.respondText(sum.toString(), ContentType.Text.Plain) + } + + get("/baseline2") { + val sum = sumQueryParams(call) + call.respondText(sum.toString(), ContentType.Text.Plain) + } + + get("/json") { + if (AppData.jsonCache.isEmpty()) { + call.respondText("Dataset not loaded", ContentType.Text.Plain, HttpStatusCode.InternalServerError) + return@get + } + call.respondBytes(AppData.jsonCache, ContentType.Application.Json) + } + + get("/compression") { + if (AppData.largeJsonCache.isEmpty()) { + call.respondText("Dataset not loaded", ContentType.Text.Plain, HttpStatusCode.InternalServerError) + return@get + } + val acceptEncoding = call.request.header(HttpHeaders.AcceptEncoding) ?: "" + if (acceptEncoding.contains("gzip") && AppData.largeGzipCache.isNotEmpty()) { + call.response.header(HttpHeaders.ContentEncoding, "gzip") + call.respondBytes(AppData.largeGzipCache, ContentType.Application.Json) + } else { + call.respondBytes(AppData.largeJsonCache, ContentType.Application.Json) + } + } + + get("/db") { + val conn = AppData.db + if (conn == null) { + call.respondText("Database not available", ContentType.Text.Plain, HttpStatusCode.InternalServerError) + return@get + } + val min = call.parameters["min"]?.toDoubleOrNull() ?: 10.0 + val max = call.parameters["max"]?.toDoubleOrNull() ?: 50.0 + + val items = mutableListOf() + synchronized(conn) { + val stmt = conn.prepareStatement( + "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50" + ) + stmt.setDouble(1, min) + stmt.setDouble(2, max) + val rs = stmt.executeQuery() + while (rs.next()) { + val tags = AppData.json.decodeFromString>(rs.getString(7)) + items.add( + DbItem( + id = rs.getInt(1), + name = rs.getString(2), + category = rs.getString(3), + price = rs.getDouble(4), + quantity = rs.getInt(5), + active = rs.getInt(6) == 1, + tags = tags, + rating = RatingInfo(score = rs.getDouble(8), count = rs.getInt(9)) + ) + ) + } + rs.close() + stmt.close() + } + val resp = DbResponse(items = items, count = items.size) + val body = AppData.json.encodeToString(DbResponse.serializer(), resp).toByteArray() + call.respondBytes(body, ContentType.Application.Json) + } + + post("/upload") { + val body = call.receiveChannel().toByteArray() + call.respondText(body.size.toString(), ContentType.Text.Plain) + } + + get("/static/{filename}") { + val filename = call.parameters["filename"] + if (filename == null) { + call.respond(HttpStatusCode.NotFound) + return@get + } + val entry = AppData.staticFiles[filename] + if (entry == null) { + call.respond(HttpStatusCode.NotFound) + return@get + } + val (data, contentType) = entry + call.respondBytes(data, ContentType.parse(contentType)) + } + } + }.start(wait = true) +} + +private fun sumQueryParams(call: ApplicationCall): Long { + var sum = 0L + call.parameters.names().forEach { name -> + call.parameters[name]?.toLongOrNull()?.let { sum += it } + } + return sum +} + +private suspend fun io.ktor.utils.io.ByteReadChannel.toByteArray(): ByteArray { + val buffer = java.io.ByteArrayOutputStream() + val tmp = ByteArray(8192) + while (!isClosedForRead) { + val read = readAvailable(tmp) + if (read <= 0) break + buffer.write(tmp, 0, read) + } + return buffer.toByteArray() +} diff --git a/frameworks/ktor/src/main/resources/logback.xml b/frameworks/ktor/src/main/resources/logback.xml new file mode 100644 index 0000000..c6e9b33 --- /dev/null +++ b/frameworks/ktor/src/main/resources/logback.xml @@ -0,0 +1,10 @@ + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + From c9a82ad147f53db97c55675f871398558bde5ce7 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:04:02 +0000 Subject: [PATCH 2/8] fix: use call.receive() for upload endpoint --- .../src/main/kotlin/com/httparena/Application.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt index d5b8a51..0a3d051 100644 --- a/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt +++ b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt @@ -243,7 +243,7 @@ fun main() { } post("/upload") { - val body = call.receiveChannel().toByteArray() + val body = call.receive() call.respondText(body.size.toString(), ContentType.Text.Plain) } @@ -273,13 +273,4 @@ private fun sumQueryParams(call: ApplicationCall): Long { return sum } -private suspend fun io.ktor.utils.io.ByteReadChannel.toByteArray(): ByteArray { - val buffer = java.io.ByteArrayOutputStream() - val tmp = ByteArray(8192) - while (!isClosedForRead) { - val read = readAvailable(tmp) - if (read <= 0) break - buffer.write(tmp, 0, read) - } - return buffer.toByteArray() -} + From 19a3c2b0292261a667b042fa5e0202f38954abe2 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:44:10 +0000 Subject: [PATCH 3/8] chore(ktor): bump to Ktor 3.4.1 + Kotlin 2.1.20 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per e5l's request — bumps all Ktor dependencies to 3.4.1 (latest) and Kotlin to 2.1.20 for compatibility. --- frameworks/ktor/build.gradle.kts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frameworks/ktor/build.gradle.kts b/frameworks/ktor/build.gradle.kts index 617ebe2..2558862 100644 --- a/frameworks/ktor/build.gradle.kts +++ b/frameworks/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - kotlin("jvm") version "2.1.10" - kotlin("plugin.serialization") version "2.1.10" - id("io.ktor.plugin") version "3.1.1" + kotlin("jvm") version "2.1.20" + kotlin("plugin.serialization") version "2.1.20" + id("io.ktor.plugin") version "3.4.1" application } @@ -17,10 +17,10 @@ repositories { } dependencies { - implementation("io.ktor:ktor-server-core:3.1.1") - implementation("io.ktor:ktor-server-netty:3.1.1") - implementation("io.ktor:ktor-server-compression:3.1.1") - implementation("io.ktor:ktor-server-default-headers:3.1.1") + implementation("io.ktor:ktor-server-core:3.4.1") + implementation("io.ktor:ktor-server-netty:3.4.1") + implementation("io.ktor:ktor-server-compression:3.4.1") + implementation("io.ktor:ktor-server-default-headers:3.4.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation("org.xerial:sqlite-jdbc:3.47.2.0") implementation("ch.qos.logback:logback-classic:1.5.15") From e99da0b1722c1b32320d9735869fb57b82e512c3 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:48:47 +0000 Subject: [PATCH 4/8] fix: stream upload body to avoid OOM at high concurrency - Use receiveChannel() + readAvailable() instead of receive() - Reads upload in 64KB chunks instead of buffering entire 20MB body - Switch to G1GC for better large allocation handling - Reduce Netty allocator maxOrder to limit pooled chunk sizes --- frameworks/ktor/Dockerfile | 3 ++- .../ktor/src/main/kotlin/com/httparena/Application.kt | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frameworks/ktor/Dockerfile b/frameworks/ktor/Dockerfile index 536eee0..58a3cab 100644 --- a/frameworks/ktor/Dockerfile +++ b/frameworks/ktor/Dockerfile @@ -11,9 +11,10 @@ COPY --from=build /app/build/libs/ktor-httparena.jar . EXPOSE 8080 ENTRYPOINT ["java", \ "-server", \ - "-XX:+UseParallelGC", \ + "-XX:+UseG1GC", \ "-XX:+UseNUMA", \ "-XX:-StackTraceInThrowable", \ "-Dio.netty.buffer.checkBounds=false", \ "-Dio.netty.buffer.checkAccessible=false", \ + "-Dio.netty.allocator.maxOrder=10", \ "-jar", "ktor-httparena.jar"] diff --git a/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt index 0a3d051..260d8e4 100644 --- a/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt +++ b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt @@ -9,6 +9,7 @@ import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.ktor.utils.io.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.io.ByteArrayOutputStream @@ -243,8 +244,14 @@ fun main() { } post("/upload") { - val body = call.receive() - call.respondText(body.size.toString(), ContentType.Text.Plain) + val channel = call.receiveChannel() + var totalBytes = 0L + val buf = ByteArray(65536) + while (!channel.isClosedForRead) { + val read = channel.readAvailable(buf) + if (read > 0) totalBytes += read + } + call.respondText(totalBytes.toString(), ContentType.Text.Plain) } get("/static/{filename}") { From e15059f627e0ab9cf950d54c24994f54875b4e2e Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:54:07 +0000 Subject: [PATCH 5/8] fix: bump Kotlin to 2.3.0 for Ktor 3.4.1 compatibility Ktor 3.4.1 was compiled with Kotlin 2.3.0 metadata (binary version 2.3.0), but the build was using Kotlin 2.1.20 which only reads up to 2.2.0. Also bump kotlinx-serialization-json to 1.8.1 for Kotlin 2.3.0 compat. --- frameworks/ktor/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frameworks/ktor/build.gradle.kts b/frameworks/ktor/build.gradle.kts index 2558862..8499f03 100644 --- a/frameworks/ktor/build.gradle.kts +++ b/frameworks/ktor/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("jvm") version "2.1.20" - kotlin("plugin.serialization") version "2.1.20" + kotlin("jvm") version "2.3.0" + kotlin("plugin.serialization") version "2.3.0" id("io.ktor.plugin") version "3.4.1" application } @@ -21,7 +21,7 @@ dependencies { implementation("io.ktor:ktor-server-netty:3.4.1") implementation("io.ktor:ktor-server-compression:3.4.1") implementation("io.ktor:ktor-server-default-headers:3.4.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") implementation("org.xerial:sqlite-jdbc:3.47.2.0") implementation("ch.qos.logback:logback-classic:1.5.15") } From 19cd2c3969fa86e4c3abc589a8b7d9306484700e Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:03:06 +0000 Subject: [PATCH 6/8] =?UTF-8?q?fix(ktor):=20remove=20-XX:-StackTraceInThro?= =?UTF-8?q?wable=20=E2=80=94=20breaks=20Kotlin=202.3.0=20static=20initiali?= =?UTF-8?q?zers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kotlin 2.3.0 has a static initializer that depends on exception stack traces for initialization checks. -XX:-StackTraceInThrowable suppresses those traces, causing ExceptionInInitializerError on first connection: java.lang.IllegalStateException: Not in static initializer. Server binds the port but can't handle any requests. Removing the flag fixes it with no measurable performance impact (stack traces are only generated on exception paths). --- frameworks/ktor/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/frameworks/ktor/Dockerfile b/frameworks/ktor/Dockerfile index 58a3cab..1984e48 100644 --- a/frameworks/ktor/Dockerfile +++ b/frameworks/ktor/Dockerfile @@ -13,7 +13,6 @@ ENTRYPOINT ["java", \ "-server", \ "-XX:+UseG1GC", \ "-XX:+UseNUMA", \ - "-XX:-StackTraceInThrowable", \ "-Dio.netty.buffer.checkBounds=false", \ "-Dio.netty.buffer.checkAccessible=false", \ "-Dio.netty.allocator.maxOrder=10", \ From 1d01630d4a149669ccb57b619f030da58b6a98d0 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:22:57 +0000 Subject: [PATCH 7/8] fix(ktor): restore -XX:-StackTraceInThrowable for 3x throughput recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark regressed from ~1M to ~315K baseline and ~3M to ~319K pipelined after removing -XX:-StackTraceInThrowable. Netty's exception-based flow control (ChannelOutboundBuffer, FastThreadLocal) generates millions of exceptions under load — filling stack traces on each one kills throughput. Fix: downgrade to Kotlin 2.1.20 which doesn't break static initializers with this JVM flag. Use -Xskip-metadata-version-check to handle Ktor 3.4.1's Kotlin 2.3.0 metadata. kotlinx-serialization adjusted to 1.8.0 for compiler compatibility. --- frameworks/ktor/Dockerfile | 1 + frameworks/ktor/build.gradle.kts | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frameworks/ktor/Dockerfile b/frameworks/ktor/Dockerfile index 1984e48..58a3cab 100644 --- a/frameworks/ktor/Dockerfile +++ b/frameworks/ktor/Dockerfile @@ -13,6 +13,7 @@ ENTRYPOINT ["java", \ "-server", \ "-XX:+UseG1GC", \ "-XX:+UseNUMA", \ + "-XX:-StackTraceInThrowable", \ "-Dio.netty.buffer.checkBounds=false", \ "-Dio.netty.buffer.checkAccessible=false", \ "-Dio.netty.allocator.maxOrder=10", \ diff --git a/frameworks/ktor/build.gradle.kts b/frameworks/ktor/build.gradle.kts index 8499f03..48462dd 100644 --- a/frameworks/ktor/build.gradle.kts +++ b/frameworks/ktor/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("jvm") version "2.3.0" - kotlin("plugin.serialization") version "2.3.0" + kotlin("jvm") version "2.1.20" + kotlin("plugin.serialization") version "2.1.20" id("io.ktor.plugin") version "3.4.1" application } @@ -21,11 +21,17 @@ dependencies { implementation("io.ktor:ktor-server-netty:3.4.1") implementation("io.ktor:ktor-server-compression:3.4.1") implementation("io.ktor:ktor-server-default-headers:3.4.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") implementation("org.xerial:sqlite-jdbc:3.47.2.0") implementation("ch.qos.logback:logback-classic:1.5.15") } +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xskip-metadata-version-check") + } +} + ktor { fatJar { archiveFileName.set("ktor-httparena.jar") From 960c7c26c321ca3c7837e7c0c2720806572c9dc8 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:29:56 +0000 Subject: [PATCH 8/8] fix(ktor): use Kotlin 2.3.0 + remove -XX:-StackTraceInThrowable The flag breaks Kotlin 2.3.0 static initializers (ExceptionInInitializerError), preventing server startup. Switch to native Kotlin 2.3.0 (matching Ktor 3.4.1's transitive dependency) and compensate with Netty-level tuning: - Disable leak detection - Pre-touch heap pages (-XX:+AlwaysPreTouch) - Auto-detect event loop threads The ~3x throughput difference from the JVM flag was real (Netty throws millions of exceptions/sec for flow control), but startup reliability is more important. Performance should still be competitive without the flag. --- frameworks/ktor/Dockerfile | 5 ++++- frameworks/ktor/build.gradle.kts | 10 ++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/frameworks/ktor/Dockerfile b/frameworks/ktor/Dockerfile index 58a3cab..eaa1f62 100644 --- a/frameworks/ktor/Dockerfile +++ b/frameworks/ktor/Dockerfile @@ -13,8 +13,11 @@ ENTRYPOINT ["java", \ "-server", \ "-XX:+UseG1GC", \ "-XX:+UseNUMA", \ - "-XX:-StackTraceInThrowable", \ + "-XX:+AlwaysPreTouch", \ "-Dio.netty.buffer.checkBounds=false", \ "-Dio.netty.buffer.checkAccessible=false", \ "-Dio.netty.allocator.maxOrder=10", \ + "-Dio.netty.leakDetection.level=disabled", \ + "-Dio.netty.recycler.maxCapacityPerThread=4096", \ + "-Dio.netty.eventLoopThreads=0", \ "-jar", "ktor-httparena.jar"] diff --git a/frameworks/ktor/build.gradle.kts b/frameworks/ktor/build.gradle.kts index 48462dd..8660ea7 100644 --- a/frameworks/ktor/build.gradle.kts +++ b/frameworks/ktor/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - kotlin("jvm") version "2.1.20" - kotlin("plugin.serialization") version "2.1.20" + kotlin("jvm") version "2.3.0" + kotlin("plugin.serialization") version "2.3.0" id("io.ktor.plugin") version "3.4.1" application } @@ -26,12 +26,6 @@ dependencies { implementation("ch.qos.logback:logback-classic:1.5.15") } -kotlin { - compilerOptions { - freeCompilerArgs.add("-Xskip-metadata-version-check") - } -} - ktor { fatJar { archiveFileName.set("ktor-httparena.jar")