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