Skip to content

KTOR-8906 Bypass Dispatchers.IO for OutputStreamContent on servlet engines#5594

Merged
bjhham merged 4 commits into
ktorio:mainfrom
amccague:jackson_blocking
May 10, 2026
Merged

KTOR-8906 Bypass Dispatchers.IO for OutputStreamContent on servlet engines#5594
bjhham merged 4 commits into
ktorio:mainfrom
amccague:jackson_blocking

Conversation

@amccague
Copy link
Copy Markdown
Contributor

@amccague amccague commented May 8, 2026

Subsystem
Servlet Servers, Jackson/Gson serialization

Motivation
In 3.x, io.ktor.utils.io.jvm.javaio.PollersKt was removed from ktor-io, causing the withBlocking bridge to always redispatch to Dispatchers.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.IO threads.

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.

…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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 717a28c5-f7ee-48b2-97a7-cfb5bd943671

📥 Commits

Reviewing files that changed from the base of the PR and between 91e0744 and c16be1a.

📒 Files selected for processing (2)
  • build-logic/src/main/kotlin/ktorbuild/targets/KtorTargets.kt
  • ktor-server/ktor-server-core/jvm/test/io/ktor/tests/hosts/ReceiveBlockingPrimitiveTest.kt
💤 Files with no reviewable changes (1)
  • ktor-server/ktor-server-core/jvm/test/io/ktor/tests/hosts/ReceiveBlockingPrimitiveTest.kt

📝 Walkthrough

Walkthrough

This PR always redispatches blocking work to Dispatchers.IO, adds an internal writeTo(OutputStream) API to OutputStreamContent, integrates direct OutputStream handling into Jetty and Servlet responses, extends test dependencies, adds concurrency-based IO exhaustion tests, and removes reflection-based parking tests.

Changes

Blocking I/O Refactoring and OutputStream API

Layer / File(s) Summary
Blocking dispatch simplification
ktor-http/jvm/src/io/ktor/http/content/BlockingBridge.kt
withBlocking now unconditionally uses withContext(Dispatchers.IO); reflective parking-detection and conditional dispatch logic removed; KDoc updated.
OutputStreamContent direct API
ktor-http/jvm/src/io/ktor/http/content/OutputStreamContent.kt, ktor-http/api/ktor-http.api
New @InternalAPI writeTo(stream: OutputStream) added to OutputStreamContent; public API entry added in ktor-http.api.
Engine integration
ktor-server/ktor-server-jetty-jakarta/jvm/src/io/ktor/server/jetty/jakarta/JettyApplicationCall.kt, ktor-server/ktor-server-servlet-jakarta/jvm/src/io/ktor/server/servlet/jakarta/ServletApplicationResponse.kt, ktor-server/ktor-server-servlet-jakarta/api/ktor-server-servlet-jakarta.api
Jetty and Servlet responses override respondWriteChannelContent to special-case OutputStreamContent and write directly to the native output stream; servlet API updated with the coroutine-based override entry.
Test dependency updates
ktor-server/ktor-server-jetty-jakarta/build.gradle.kts, ktor-server/ktor-server-tomcat-jakarta/build.gradle.kts
Added jvmTest dependencies for Ktor content negotiation and Jackson (including Kotlin module) to enable Jackson-based tests.
New concurrency tests
ktor-server/ktor-server-jetty-jakarta/jvm/test/.../JettySerializationIOExhaustionTest.kt, ktor-server/ktor-server-tomcat-jakarta/jvm/test/.../TomcatSerializationIOExhaustionTest.kt
New tests issue many concurrent POSTs (/json/jackson) to stress Jackson deserialization on Dispatchers.IO and assert all responses are 200 OK within bounded timeouts (50 concurrent for Jetty, 30 for Tomcat).
Test cleanup
ktor-server/ktor-server-core/jvm/test/io/ktor/tests/hosts/ReceiveBlockingPrimitiveTest.kt, ktor-shared/ktor-serialization/*/jvm/test/*Server*BlockingTest.kt
Removed reflection-based parking-prohibition logic and entire blocking tests that used UnsafeDispatcher; those tests and reflective helpers were deleted.
Build logic
build-logic/src/main/kotlin/ktorbuild/targets/KtorTargets.kt
filterTargets now composes predicates by capturing the previous filter into a local previousFilter before combining.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ktorio/ktor#5173: Both PRs modify KtorTargets (including the filterTargets logic), so the main PR’s change to filter composition is directly related.
  • ktorio/ktor#5301: Touches parking-detection and UnsafeDispatcher test usage related to blocking/parking detection.

Suggested labels

👍 ship!

Suggested reviewers

  • bjhham
  • e5l
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a bypass for Dispatchers.IO when writing OutputStreamContent on servlet engines, which is the core objective of this PR.
Description check ✅ Passed The description follows the required template structure with Subsystem, Motivation, and Solution sections. It comprehensively explains the problem (PollersKt removal), the solution (direct OutputStream writing), and related cleanup (removing reflection code and tests).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_000 is unreachable – the outer withTimeout(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

📥 Commits

Reviewing files that changed from the base of the PR and between 4766120 and 5d63fb9.

📒 Files selected for processing (12)
  • ktor-http/jvm/src/io/ktor/http/content/BlockingBridge.kt
  • ktor-http/jvm/src/io/ktor/http/content/OutputStreamContent.kt
  • ktor-server/ktor-server-core/jvm/test/io/ktor/tests/hosts/ReceiveBlockingPrimitiveTest.kt
  • ktor-server/ktor-server-jetty-jakarta/build.gradle.kts
  • ktor-server/ktor-server-jetty-jakarta/jvm/src/io/ktor/server/jetty/jakarta/JettyApplicationCall.kt
  • ktor-server/ktor-server-jetty-jakarta/jvm/test/io/ktor/tests/server/jetty/jakarta/JettySerializationIOExhaustionTest.kt
  • ktor-server/ktor-server-servlet-jakarta/jvm/src/io/ktor/server/servlet/jakarta/ServletApplicationResponse.kt
  • ktor-server/ktor-server-tomcat-jakarta/build.gradle.kts
  • ktor-server/ktor-server-tomcat-jakarta/jvm/test/io/ktor/tests/server/tomcat/jakarta/TomcatSerializationIOExhaustionTest.kt
  • ktor-shared/ktor-serialization/ktor-serialization-gson/jvm/test/ServerGsonBlockingTest.kt
  • ktor-shared/ktor-serialization/ktor-serialization-jackson/jvm/test/ServerJacksonBlockingTest.kt
  • ktor-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

Comment thread ktor-http/jvm/src/io/ktor/http/content/OutputStreamContent.kt
Copy link
Copy Markdown
Contributor

@bjhham bjhham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

amccague added 2 commits May 10, 2026 14:17
The filter lambda captured itself by reference, causing infinite
recursion when resolved. Capture previous value before reassignment.
@amccague
Copy link
Copy Markdown
Contributor Author

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!

Needed to fix a stackoverflow error on running the command, done!

Copy link
Copy Markdown
Contributor

@bjhham bjhham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@bjhham bjhham merged commit 7ed7bf0 into ktorio:main May 10, 2026
17 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants