Skip to content

CoroutineDispatchers-compatible ContinuationInterceptor composition #2478

Closed
@yorickhenning

Description

@yorickhenning

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. :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions