kotlinx.coroutines: Possible bug with `ThreadLocal.asContextElement`
Hello, I’m observing a strange behavior with the following code:
val t: ThreadLocal<String> = ThreadLocal.withInitial { "a" }
@Test
fun testContext1() = runBlocking {
println(t.get())
withContext(coroutineContext.plus(t.asContextElement("b"))) {
println(t.get())
}
println(t.get())
}
@Test
fun testContext2() = runBlocking {
println(t.get())
withContext(coroutineContext.plus(t.asContextElement("b"))) {
println(t.get())
delay(Random.nextLong(100))
println(t.get())
}
println(t.get())
}
@Test
fun testContext3() = runBlocking {
println(t.get())
async(coroutineContext.plus(t.asContextElement("b"))) {
println(t.get())
delay(Random.nextLong(100))
println(t.get())
}.await()
println(t.get())
}
The result is:
testContext1printsaba- expectedtestContext2printsabbb- It looks like the value is not restored for some reason. Is this a bug? What’s the explanation?testContext3printsabba- expected
Please help!
I’m using org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 6
- Comments: 34 (11 by maintainers)
Commits related to this issue
- Clarify thread locals documentation Fixes #985 — committed to Kotlin/kotlinx.coroutines by qwwdfsad 5 years ago
- Clarify thread locals documentation Fixes #985 — committed to Kotlin/kotlinx.coroutines by qwwdfsad 5 years ago
- Clarify thread locals documentation Fixes #985 — committed to Kotlin/kotlinx.coroutines by qwwdfsad 5 years ago
- Restore thread context elements when directly resuming to parent (WIP) Note, that this fix has potentially severe performance impact since it always walk up the coroutine completion stack in search f... — committed to Kotlin/kotlinx.coroutines by elizarov 5 years ago
- Restore thread context elements when directly resuming to parent (WIP) Note, that this fix has potentially severe performance impact since it always walk up the coroutine completion stack in search f... — committed to Kotlin/kotlinx.coroutines by elizarov 5 years ago
- Restore thread context elements when directly resuming to parent (WIP) Note, that this fix has potentially severe performance impact since it always walk up the coroutine completion stack in search f... — committed to Kotlin/kotlinx.coroutines by elizarov 5 years ago
- Restore thread context elements when directly resuming to parent (WIP) Note, that this fix has potentially severe performance impact since it always walk up the coroutine completion stack in search f... — committed to Kotlin/kotlinx.coroutines by elizarov 5 years ago
- Restore thread context elements when directly resuming to parent This fix solves the problem of restoring thread-context when returning to another context in undispatched way. It impacts suspend/resu... — committed to Kotlin/kotlinx.coroutines by elizarov 5 years ago
- Restore thread context elements when directly resuming to parent This fix solves the problem of restoring thread-context when returning to another context in undispatched way. It impacts suspend/resu... — committed to Kotlin/kotlinx.coroutines by elizarov 5 years ago
- Restore thread context elements when directly resuming to parent This fix solves the problem of restoring thread-context when returning to another context in undispatched way. It impacts suspend/resu... — committed to Kotlin/kotlinx.coroutines by elizarov 5 years ago
- Restore thread context elements when directly resuming to parent. This fix solves the problem of restoring thread-context when returning to another context in undispatched way. It impacts suspend/re... — committed to Kotlin/kotlinx.coroutines by qwwdfsad 5 years ago
- Restore thread context elements when directly resuming to parent. This fix solves the problem of restoring thread-context when returning to another context in undispatched way. It impacts suspend/re... — committed to Kotlin/kotlinx.coroutines by qwwdfsad 5 years ago
- Restore thread context elements when directly resuming to parent (#1577) This fix solves the problem of restoring thread-context when returning to another context in an undispatched way. It imp... — committed to Kotlin/kotlinx.coroutines by elizarov 3 years ago
Until there’s a new release, it would be great to have a clear example of what this “expected usage” looks like. Specifically, the documentation for
kotlinx-coroutines-slf4jis extremely short and could benefit from a warning about the caveat caused by this bug and an example of a workaround. Might be best tracked as a separate issue, not sure.Yes, this is the existing limitation of
MDCContextexposed more strictly.Quoting our KDoc:
This is a by-design limitation, that become much more clearer when more than one coroutine is launched.
E.g.:
do you expect mutated MDC to be available in
runBlockingcoroutine. What ifsomeDispatcherisDispatchers.Unconfinedand what if it’s concurrent? And what if it’s concurrent, but it happened to be dispatched on the same thread asrunBlockingcoroutine?Yes, we are going to release the fix with the next release
Hello,
I am experiencing the same issue even with the code base that is not huge and is mostly under our control. Our services use MDC logging, opencensus tracing, thread locals, and GRPC contexts. It has been a huge challenge to wire everything up correctly to work with coroutine context propagation.
For the code under our control, we need to remember to seed all the different context elements every time somebody starts a coroutine. This leads to tight coupling, every piece of code that wants to start a coroutine needs to be aware of all the thread context elements in the entire codebase. And of course, it is very error-prone.
The problem is exacerbated by GRPC launching coroutines without giving everyone the opportunity to override the behavior. I imagine other libraries wanting to do the same.
The described behavior is surprising, to say the least. I don’t totally understand why global injection is needed to fix this. In the examples above, there are clear scope/context entry and exit points. Could you explain why the
restoreThreadContextis not invoked at the block’s end? It’s especially surprising behavior because if you removedelayinvocation from the examples above,withContextworks as expected!Are there any updates on this issue?
We assume that
kotlinx-coroutines-slf4jis used in the following way. Consider the application that is using SLF4J for logging, uses coroutines, and uses MDC. This application addskotlinx-coroutines-slf4jto its dependencies and addsMDCContext(emptyMap())to its top-level scope to give a “signal” to coroutine machinery that it is going to useMDCContext. After that, its MDC will be always correctly set/restored and reset and switching between different coroutines.Unfortunately, this does not currently affect
GlobalScopeand requires some boiler-plate code from the application writer. E.g., if my application has code likeval myScope = CoroutineScope(someContext)then I, as an application author, have to remember to addMDCContext(emptyMap())to this scope’s context.What we can do to improve this situation is to give integration modules like
kotlinx-coroutines-slf4jand ability to globally inject context elements to all scopes (includingGlobalScope). This should considerably improve usability. All you’ll need to do in this case is to addkotlinx-coroutines-slf4jto dependencies and that’s it. What do you think?P.S. The downside of this approach to consider is that anyone who adds
kotlinx-coroutines-slf4jto their dependencies will get a performance hit even if they don’t use MDC (as an afterthought – there’s no other reason to addkotlinx-coroutines-slf4j, but for MDC support).It would also be nice to add the warning to the documentation of
ThreadContextElementtoo. At the moment it does not mention thatrestoreThreadContext()may not be called when it would be expected by users implementingThreadContextElementthemselves.And it would be much better to have thread state restored outside
withContext { }, because a library may not have control over initial context of the coroutine, so this feature becomes unusable there.