kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

Ability to obtain the current coroutine context in arbitrary code

Open dovchinnikov opened this issue 2 years ago • 6 comments

In IJ we have huge amount of old Java code. Sometimes we want to track certain behaviour or turn on assertions depending on whether the code is running under a coroutine or not, or we'd like to access some value which is put into the coroutine context by the platform. With coroutines started by the platform we can achieve this by putting a simple ThreadContextElement on the platform side:

val ourCoroutineContext = ThreadLocal<CoroutineContext>()

object ThreadCoroutineContextElement :
  ThreadContextElement<Unit>,
  CoroutineContext.Key<ThreadCoroutineContextElement> {

  override val key: CoroutineContext.Key<*>
    get() = ThreadCoroutineContextElement

  override fun updateThreadContext(context: CoroutineContext) {
    ourCoroutineContext.set(context)
  }

  override fun restoreThreadContext(context: CoroutineContext, oldState: Unit) {
    ourCoroutineContext.remove()
  }
}

Cons of this approach:

  • we cannot do this for all other coroutines which are launched in GlobalScope and CoroutineScope(EmptyCoroutineContext) outside of the platform, or runBlocking.
  • existence of ThreadContextElement in the context may trigger the slow path in kotlinx.coroutines.internal.ThreadContextKt#updateThreadContext, it won't be necessary if this behaviour could be natively supported by the library.

Please provide an API for obtaining the current coroutine context inside arbitrary code. I think this behaviour might be turned on or off by a system property. Alternatively, a ServiceLoader-based API for contributing to the default coroutine context would be also helpful.

dovchinnikov avatar Nov 08 '22 13:11 dovchinnikov

a ServiceLoader-based API for contributing to the default coroutine context

I've just discovered #2932

dovchinnikov avatar Oct 10 '23 21:10 dovchinnikov

https://github.com/JetBrains/intellij-deps-kotlinx.coroutines/commit/0103dd5694cb67f5790a41c0e02cdbc38829735b

dovchinnikov avatar Jun 13 '24 15:06 dovchinnikov

I can't imagine how it could be implemented other than by always keeping a thread-local value with the current context that would need to be updated on every dispatch, and it seems like that's the approach you took in https://github.com/JetBrains/intellij-deps-kotlinx.coroutines/commit/0103dd5694cb67f5790a41c0e02cdbc38829735b.

I think the ServiceLoader-based API is the better way: with a global flag, when some parts of the program don't need this behavior, you can't do anything. If, on the other hand, you have a coroutine context element, you can remove it from the context where you don't need it. Other than this added flexibility, I don't see meaningful differences between the two approaches if we sufficiently limit the coroutine context elements suitable for the ServiceLoader API.

dkhalanskyjb avatar Jul 10 '24 12:07 dkhalanskyjb

you can remove it from the context where you don't need it

How? Override with an empty element with the same key? I'd like to have a proper way of actually removing an element from the context, but I understand that it might be dangerous in general.

dovchinnikov avatar Jul 11 '24 10:07 dovchinnikov

Override with an empty element with the same key?

Yep. Also, not in this case, but often, something like CoroutineContext(oldContext.minusKey(unnecessaryKey)) can help.

dkhalanskyjb avatar Jul 11 '24 10:07 dkhalanskyjb

It would be great to have withoutContext(Key) {}

dovchinnikov avatar Jul 11 '24 10:07 dovchinnikov