Description
I'm integrating coroutines with an existing Java/JVM ThreadLocal
-centric function tracing system.
When a plain Java stack frame launches a coroutine, it needs to get its trace from the caller. Passing it as part of the context is natural:
injectedScope.launch(
getTrace() // CoroutineContext.Element.
) {
}
... And with a ThreadContextElement
, the CoroutineScope
saves and restores ThreadLocal
so things are transparent to Java code the coroutine calls.
Manually passing the trace at each coroutine launch is more error-prone than I'd like. Given codebases with thousands of launch sites, trace handoff can get forgotten. I can use runtime checks and static analysis, but coroutines should make it possible to intercept resumptions and make tracing transparent rather than manual.
Omitting some frames, the call stack for a dispatched coroutine at start time is:
dispatch:93, ExecutorCoroutineDispatcherBase (kotlinx.coroutines)
resumeWith:184, DispatchedContinuation (kotlinx.coroutines.internal)
resumeCancellableWith:266, DispatchedContinuationKt (kotlinx.coroutines.internal)
startCoroutineCancellable:30, CancellableKt (kotlinx.coroutines.intrinsics)
invoke:109, CoroutineStart (kotlinx.coroutines)
start:158, AbstractCoroutine (kotlinx.coroutines)
launch:1, BuildersKt (kotlinx.coroutines)
fancyClientFunction:99, SomeTest (com.google.stuff)
I'd like to be able to write a composed interceptor to create the context for resumptions, only without reimplementing the whole dispatcher stack. So, something like:
dispatch:93, ExecutorCoroutineDispatcherBase (kotlinx.coroutines)
resumeWith:184, DispatchedContinuation (kotlinx.coroutines.internal)
resumeCancellableWith:266, DispatchedContinuationKt (kotlinx.coroutines.internal)
***resumeWith:10, UserDefinedContinuation (com.google.stuff)***
startCoroutineCancellable:30, CancellableKt (kotlinx.coroutines.intrinsics)
invoke:109, CoroutineStart (kotlinx.coroutines)
start:158, AbstractCoroutine (kotlinx.coroutines)
launch:1, BuildersKt (kotlinx.coroutines)
fancyClientFunction:99, SomeTest (com.google.stuff)
This is how I thought ContinuationInterceptor
might work when I first read it, but CoroutineDispatcher
inherits from ContinuationInterceptor
and occupies its CoroutineContext.Key
. Composition is doubly-incompatible with dispatchers, because CoroutineDispatcher.interceptContinuation()
is a final method returning a library-internal
class instance, and optimizations care about what the type of the continuation is.
It'd be nice if user-defined interceptors could compose with the CoroutineDispatcher
interceptions, allowing transparent modification of coroutine context with dynamic (albeit static/threadlocal) state.
It'd be even nicer if continuation interceptors could modify the CoroutineContext
after the continuation executes, so they could maintain state without locking. I've debugged intercept a bit, and I think the lock-free resumption/suspension in the JVM dispatch stack and (maybe?) reusable continuations have led to resumeWith
executing concurrently for the same coroutine and its Continuation
object. The resumeWith()
stack frame can still be unwinding on one thread, while the continuation has been redispatched and the same object's resumeWith()
has begun execution on another thread. That makes state handoff hard without locking.
I can make things work having each call site pass its trace to the CoroutineContext
, but a properly transparent API would have been better. It's so close. I'd only have to reimplement most of kotlinx-coroutines-core/jvm
. :)