Lucas Milotich
05/06/2024, 3:48 PMStatusPages and I want to extract some parameters from the coroutineContext but every time that I want to access to it from StatusPages , it’s null. If I try to access before StatusPages kicks in, I can find the value. Walking through the code I found that StatusPages is using CallFailed hook and I found the following:
public object CallFailed : Hook<suspend (call: ApplicationCall, cause: Throwable) -> Unit> {
private val phase = PipelinePhase("BeforeSetup")
override fun install(
pipeline: ApplicationCallPipeline,
handler: suspend (call: ApplicationCall, cause: Throwable) -> Unit
) {
pipeline.insertPhaseBefore(ApplicationCallPipeline.Setup, phase)
pipeline.intercept(phase) {
try {
coroutineScope {
proceed() // Comment added by me: This is a suspend call
}
} catch (cause: Throwable) {
handler(call, cause) // Comment added by me: This is a suspend call
if (!call.response.isSent) throw cause
}
}
}
}
Therefore, if I throw an exception, this one is intercepted by the catch and the handler function is executed without a specific coroutineScope.
So, mine main question is: Shouldn’t the handler us the coroutineScope as well in order to inherit the scope from the caller? (my assumption is that during normal execution flow I can access the coroutineContext because of the coroutineScope call)AdamW
05/06/2024, 4:30 PMLucas Milotich
05/06/2024, 4:32 PMCoroutineContext.Key :
class SpanPropagator {
data class SpanPropagationContext(val span: Span) :
AbstractCoroutineContextElement(SpanPropagationContext) {
companion object Key : CoroutineContext.Key<SpanPropagationContext>
}
class Config
companion object Plugin : BaseApplicationPlugin<Application, Config, SpanPropagator> {
override val key: AttributeKey<SpanPropagator> = AttributeKey("SpanPropagator")
override fun install(pipeline: Application, configure: Config.() -> Unit): SpanPropagator {
val plugin = SpanPropagator()
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
val tracer = GlobalTracer.get()
val span =
tracer.activeSpan() ?: GlobalTracer.get().buildSpan("server.request").start()
val scope = tracer.activateSpan(span)
try {
withContext(coroutineContext + MDCContext() + SpanPropagationContext(span)) {
proceed()
}
} finally {
scope.close()
finish()
}
}
return plugin
}
}
}
fun CoroutineContext.getSpan() = this[SpanPropagationContext]?.span
This is installed with install and then from StatusPages configuration I’m doing:
val activeSpan = coroutineContext.getSpan()AdamW
05/06/2024, 4:34 PMLucas Milotich
05/06/2024, 4:38 PMAdamW
05/06/2024, 4:39 PMLucas Milotich
05/06/2024, 4:40 PMLucas Milotich
05/06/2024, 4:40 PMstatusPages can access the coroutineContextAdamW
05/06/2024, 4:42 PMcoroutineContext in general, I do something vaguely similar in that I’m accessing an element in the exception handler.Lucas Milotich
05/07/2024, 8:13 AMcoroutineContext in the try but not in the catchAdamW
05/07/2024, 8:22 AMcoroutineScope, it just creates a new Job for proceed. The outer coroutineContext is implicitly available in handler, so it’s likely not that. Note that the coroutine may proceed on a different thread.Aleksei Tirman [JB]
05/07/2024, 9:18 AMAttributes (ApplicationCall.attributes) which have a lifetime of the call.Lucas Milotich
05/07/2024, 9:38 AMStatusPages to the call attributes. Do you know if this is something expected or could be a bug in my code? perhaps I can create an empty ktor app with status page and test itAleksei Tirman [JB]
05/07/2024, 9:43 AMembeddedServer(Netty, port = 3333) {
install(StatusPages) {
exception<RuntimeException> { call, cause ->
println(cause)
println(kotlin.coroutines.coroutineContext)
}
}
routing {
get {
throw RuntimeException()
}
}
intercept(ApplicationCallPipeline.Monitoring) {
withContext(SpanPropagationContext()) {
println(coroutineContext)
proceed()
}
}
}.start(wait = true)
As a result, I observe your described behavior but I am not sure if that's expected or not.Aleksei Tirman [JB]
05/13/2024, 8:11 AMLucas Milotich
05/13/2024, 8:19 AM