KTOR-8906 Bypass Dispatchers.IO for OutputStreamContent on servlet engines#5594
Conversation
…gines Restores Ktor 2.x behavior where serialization on servlet engines ran in-place on the request thread without consuming Dispatchers.IO threads. In 3.x, PollersKt was removed from ktor-io, causing the withBlocking bridge to always redispatch to Dispatchers.IO. Combined with the ByteWriteChannel drain coroutine also needing IO, this created a circular thread-pool dependency that permanently deadlocked under concurrent load. Servlet-based engines (Jetty, Tomcat) now write OutputStreamContent directly to the native OutputStream via a new @internalapi overload, bypassing both the IO dispatch and the runBlocking bridge entirely. Also removes the dead isParkingAllowed reflection that has been a no-op since the PollersKt class was deleted in the ktor-io 3.x rewrite, along with the redundant ServerJacksonBlockingTest/ServerGsonBlockingTest tests that existed solely to exercise that mechanism.
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughThis PR always redispatches blocking work to Dispatchers.IO, adds an internal ChangesBlocking I/O Refactoring and OutputStream API
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
ktor-server/ktor-server-tomcat-jakarta/jvm/test/io/ktor/tests/server/tomcat/jakarta/TomcatSerializationIOExhaustionTest.kt (1)
56-58: 💤 Low value
requestTimeoutMillis = 60_000is unreachable – the outerwithTimeout(30.seconds)always fires first.If any request stalls, the outer 30-second scope cancellation will trigger before the 60-second per-request timeout. The Jetty counterpart consistently uses 60 seconds for both. Consider aligning these, or setting the per-request timeout to ≤ 30 s to match the outer bound.
♻️ Proposed fix
- install(HttpTimeout) { - requestTimeoutMillis = 60_000 - } + install(HttpTimeout) { + requestTimeoutMillis = 30_000 + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ktor-server/ktor-server-tomcat-jakarta/jvm/test/io/ktor/tests/server/tomcat/jakarta/TomcatSerializationIOExhaustionTest.kt` around lines 56 - 58, The per-request timeout set in the test (install(HttpTimeout) { requestTimeoutMillis = 60_000 }) is ineffective because the outer withTimeout(30.seconds) cancels first; update the HttpTimeout configuration so requestTimeoutMillis is <= 30_000 (or change the outer withTimeout to 60.seconds if you intend a 60s per-request bound) so the per-request setting is reachable—locate the install(HttpTimeout) block and the surrounding withTimeout(...) call in TomcatSerializationIOExhaustionTest and make them consistent (either lower requestTimeoutMillis to 30_000 or raise the withTimeout duration to 60.seconds).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ktor-http/jvm/src/io/ktor/http/content/OutputStreamContent.kt`:
- Around line 45-48: A new public method OutputStreamContent.writeTo(stream:
OutputStream) was added, so update the Kotlin ABI signature dump: run ./gradlew
:ktor-http:updateKotlinAbi to regenerate api/ktor-http.api, review the changes
to ensure the new signature for OutputStreamContent.writeTo is present, and
commit the updated api/ktor-http.api file alongside your code change.
In
`@ktor-server/ktor-server-servlet-jakarta/jvm/src/io/ktor/server/servlet/jakarta/ServletApplicationResponse.kt`:
- Around line 52-61: The respondWriteChannelContent implementation prematurely
closes and commits the servlet response by using
servletResponse.outputStream.use { … }, which leaves responseJob uninitialized
and triggers a fallback that later calls flushBuffer() on a committed response;
change respondWriteChannelContent (handling of OutputStreamContent) to write to
servletResponse.outputStream without closing it (do not call .use or close the
stream) so the existing init pipeline interceptor can perform a single
flush/commit and responseJob remains initialized as in other WriteChannelContent
paths.
---
Nitpick comments:
In
`@ktor-server/ktor-server-tomcat-jakarta/jvm/test/io/ktor/tests/server/tomcat/jakarta/TomcatSerializationIOExhaustionTest.kt`:
- Around line 56-58: The per-request timeout set in the test
(install(HttpTimeout) { requestTimeoutMillis = 60_000 }) is ineffective because
the outer withTimeout(30.seconds) cancels first; update the HttpTimeout
configuration so requestTimeoutMillis is <= 30_000 (or change the outer
withTimeout to 60.seconds if you intend a 60s per-request bound) so the
per-request setting is reachable—locate the install(HttpTimeout) block and the
surrounding withTimeout(...) call in TomcatSerializationIOExhaustionTest and
make them consistent (either lower requestTimeoutMillis to 30_000 or raise the
withTimeout duration to 60.seconds).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1888e98f-d18c-467c-bf07-17091b0d9c61
📒 Files selected for processing (12)
ktor-http/jvm/src/io/ktor/http/content/BlockingBridge.ktktor-http/jvm/src/io/ktor/http/content/OutputStreamContent.ktktor-server/ktor-server-core/jvm/test/io/ktor/tests/hosts/ReceiveBlockingPrimitiveTest.ktktor-server/ktor-server-jetty-jakarta/build.gradle.ktsktor-server/ktor-server-jetty-jakarta/jvm/src/io/ktor/server/jetty/jakarta/JettyApplicationCall.ktktor-server/ktor-server-jetty-jakarta/jvm/test/io/ktor/tests/server/jetty/jakarta/JettySerializationIOExhaustionTest.ktktor-server/ktor-server-servlet-jakarta/jvm/src/io/ktor/server/servlet/jakarta/ServletApplicationResponse.ktktor-server/ktor-server-tomcat-jakarta/build.gradle.ktsktor-server/ktor-server-tomcat-jakarta/jvm/test/io/ktor/tests/server/tomcat/jakarta/TomcatSerializationIOExhaustionTest.ktktor-shared/ktor-serialization/ktor-serialization-gson/jvm/test/ServerGsonBlockingTest.ktktor-shared/ktor-serialization/ktor-serialization-jackson/jvm/test/ServerJacksonBlockingTest.ktktor-shared/ktor-serialization/ktor-serialization-jackson3/jvm/test/ServerJacksonBlockingTest.kt
💤 Files with no reviewable changes (4)
- ktor-shared/ktor-serialization/ktor-serialization-jackson3/jvm/test/ServerJacksonBlockingTest.kt
- ktor-shared/ktor-serialization/ktor-serialization-jackson/jvm/test/ServerJacksonBlockingTest.kt
- ktor-shared/ktor-serialization/ktor-serialization-gson/jvm/test/ServerGsonBlockingTest.kt
- ktor-server/ktor-server-core/jvm/test/io/ktor/tests/hosts/ReceiveBlockingPrimitiveTest.kt
bjhham
left a comment
There was a problem hiding this comment.
Just a couple of style comments, and could you please run ./gradlew formatKotlin to make the linter happy?
Other than that it looks good, thank you for the fix!
The filter lambda captured itself by reference, causing infinite recursion when resolved. Capture previous value before reassignment.
Needed to fix a stackoverflow error on running the command, done! |
Subsystem
Servlet Servers, Jackson/Gson serialization
Motivation
In 3.x,
io.ktor.utils.io.jvm.javaio.PollersKtwas removed from ktor-io, causing thewithBlockingbridge to always redispatch toDispatchers.IO.https://youtrack.jetbrains.com/issue/KTOR-8906
Solution
Restores Ktor 2.x behavior where serialization on servlet engines ran in-place on the request thread without consuming
Dispatchers.IOthreads.Servlet-based engines (Jetty, Tomcat) now write
OutputStreamContentdirectly to the nativeOutputStreamvia a new@InternalAPIoverload, bypassing both the IO dispatch and the runBlocking bridge entirely.Also removes the dead
isParkingAllowedreflection that has been a no-op since the PollersKt class was deleted in the ktor-io 3.x rewrite, along with the redundantServerJacksonBlockingTest/ServerGsonBlockingTesttests that existed solely to exercise that mechanism.