fix(ktor): resolve client hang on streaming path with Ktor 3.5.0#18781
Conversation
|
|
Two changes to fix requests hanging when using prepareGet().execute {}:
1. Request pipeline: replace withContext(asContextElement()) { proceed() }
with attribute-based context passing. Ktor 3.5.0 (KTOR-9546) added a
responseJobHolder as a child of the call job in DefaultTransform, which
delays call job completion. Combined with DebugPipelineContext (hardcoded
on for client pipelines), withContext wrapping proceed() at request
pipeline scope prevents the call from returning promptly. The parent
context is now passed via request attributes to the send pipeline where
createSpan consumes it.
2. Receive pipeline: replace scope.launch { job.join(); endSpan() } with
job.invokeOnCompletion { endSpan() }. The launched coroutine waited on
the call job which no longer completes promptly due to KTOR-9546's
responseJobHolder child. invokeOnCompletion fires synchronously when
the job reaches terminal state without suspending or leaking coroutines.
9f35a4f to
990ffb4
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses Ktor 3.5.0 client hangs on streaming prepareGet().execute {} calls by avoiding coroutine context wrapping at request-pipeline scope and ending spans via job completion callbacks.
Changes:
- Pass the parent OpenTelemetry context through Ktor request attributes into span creation.
- Replace the receive-pipeline
launch { job.join() }span-end flow withinvokeOnCompletion. - Add a streaming regression test and update Ktor 3 test/runtime dependencies to 3.5.0.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
instrumentation/ktor/ktor-common-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/common/v2_0/internal/KtorClientTelemetryUtil.kt |
Updates context propagation and span-ending logic for Ktor client instrumentation. |
instrumentation/ktor/ktor-common-2.0/library/src/main/kotlin/io/opentelemetry/instrumentation/ktor/common/v2_0/AbstractKtorClientTelemetry.kt |
Adds a createSpan overload accepting an explicit parent context. |
instrumentation/ktor/ktor-3.0/testing/build.gradle.kts |
Raises shared Ktor 3 testing dependencies to 3.5.0. |
instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/KtorHttpClientStreamingTest.kt |
Adds a regression test for streaming client requests with telemetry installed. |
instrumentation/ktor/ktor-3.0/library/build.gradle.kts |
Raises Ktor 3 library/test dependency version to 3.5.0. |
instrumentation/ktor/ktor-3.0/javaagent/build.gradle.kts |
Raises Ktor 3 javaagent library/test dependency version to 3.5.0. |
Comments suppressed due to low confidence (1)
instrumentation/ktor/ktor-3.0/library/src/test/kotlin/io/opentelemetry/instrumentation/ktor/v3_0/KtorHttpClientStreamingTest.kt:75
- [Testing] This regression test only verifies that the streaming request returns; it does not assert that the client span is actually ended/exported after the
invokeOnCompletionpath runs. Since this PR changes span-ending behavior for this exact path, the test would still pass if the request completed but telemetry was silently dropped.
client.prepareGet("http://localhost:$serverPort/success").execute { response ->
response.bodyAsText()
}
83d8249 to
0926472
Compare
| import kotlin.time.Duration.Companion.nanoseconds | ||
| import kotlin.time.Duration.Companion.seconds | ||
|
|
||
| class KtorHttpClientStreamingTest { |
There was a problem hiding this comment.
did you consider adding this test to AbstractKtorHttpClientTest?
There was a problem hiding this comment.
I've done this, I prefer it a bit more self-contained because of the pre-requisites but have a look at what I've done with comments to explain themselves.
#18779
Two changes to fix requests hanging when using
prepareGet().execute {}:Request pipeline: replace
withContext(asContextElement()) { proceed() }with attribute-based context passing. Ktor 3.5.0 (KTOR-9546) added a responseJobHolder as a child of the call job inDefaultTransform, which delays call job completion.withContextwrappingproceed()at request pipeline scope prevents the call from returning promptly. The parent context is now passed via request attributes to the send pipeline where createSpan consumes it.Receive pipeline: replace
scope.launch { job.join(); endSpan() }withjob.invokeOnCompletion { endSpan() }. The launched coroutine waited on the call job which no longer completes promptly due to KTOR-9546'sresponseJobHolderchild. invokeOnCompletion fires synchronously when the job reaches terminal state without suspending or leaking coroutines.