Skip to content

Commit 0103dd5

Browse files
committed
Allow introspection into coroutine context from non-suspending environment
This is required for https://youtrack.jetbrains.com/issue/IJPL-445
1 parent 0c86700 commit 0103dd5

File tree

11 files changed

+158
-6
lines changed

11 files changed

+158
-6
lines changed

IntelliJ-patches.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,21 @@
11
# Included IntelliJ-related patches
2-
- TODO
2+
3+
## Introspection of `coroutineContext`
4+
5+
### Description:
6+
To support the threading framework of IntelliJ,
7+
we need to cooperate with kotlin coroutines better than they allow it by default.
8+
9+
One example of this cooperation is an ability to get coroutine context from non-suspending places.
10+
Essentially, we put the coroutine context into a thread local variable on every coroutine resumption,
11+
which allows us to read necessary information without a significant change in semantics.
12+
This change has a mild performance penalty, namely, modification of a thread local variable.
13+
However, coroutines themselves use thread local states via `ThreadLocalContextElement`, which hints that
14+
one more thread local variable would not harm.
15+
16+
### API:
17+
18+
We provide a single method `kotlinx.coroutines.internal.intellij.IntellijCoroutines.currentThreadCoroutineContext`.
19+
The invariant is that the result of this method is always equal to `coroutineContext` in suspending environment,
20+
and it does not change during the non-suspending execution within the same thread.
21+

kotlinx-coroutines-core/common/src/CoroutineContext.common.kt

+3
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ internal expect inline fun <T> withCoroutineContext(context: CoroutineContext, c
2525
internal expect inline fun <T> withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T
2626
internal expect fun Continuation<*>.toDebugString(): String
2727
internal expect val CoroutineContext.coroutineName: String?
28+
29+
// added by IntelliJ
30+
internal expect inline fun <T> withThreadLocalContext(context: CoroutineContext, block: () -> T) : T

kotlinx-coroutines-core/common/src/internal/Scopes.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ internal open class ScopeCoroutine<in T>(
2525

2626
override fun afterResume(state: Any?) {
2727
// Resume direct because scope is already in the correct context
28-
uCont.resumeWith(recoverResult(state, uCont))
28+
withThreadLocalContext(uCont.context) {
29+
uCont.resumeWith(recoverResult(state, uCont))
30+
}
2931
}
3032
}
3133

kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ private inline fun <T> startDirect(completion: Continuation<T>, block: (Continua
5858
*/
5959
internal fun <T, R> ScopeCoroutine<T>.startUndispatchedOrReturn(receiver: R, block: suspend R.() -> T): Any? {
6060
return undispatchedResult({ true }) {
61-
block.startCoroutineUninterceptedOrReturn(receiver, this)
61+
withThreadLocalContext(this.context) {
62+
block.startCoroutineUninterceptedOrReturn(receiver, this)
63+
}
6264
}
6365
}
6466

kotlinx-coroutines-core/js/src/CoroutineContext.kt

+2
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ internal actual class UndispatchedCoroutine<in T> actual constructor(
5555
) : ScopeCoroutine<T>(context, uCont) {
5656
override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont))
5757
}
58+
59+
internal actual inline fun <T> withThreadLocalContext(context: CoroutineContext, block: () -> T) : T = block()

kotlinx-coroutines-core/jvm/src/CoroutineContext.kt

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kotlinx.coroutines
22

33
import kotlinx.coroutines.internal.*
4+
import kotlinx.coroutines.internal.intellij.currentContextThreadLocal
45
import kotlin.coroutines.*
56
import kotlin.coroutines.jvm.internal.CoroutineStackFrame
67

@@ -90,12 +91,22 @@ private fun foldCopies(originalContext: CoroutineContext, appendContext: Corouti
9091
internal actual inline fun <T> withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T {
9192
val oldValue = updateThreadContext(context, countOrElement)
9293
try {
93-
return block()
94+
return withThreadLocalContext(context, block)
9495
} finally {
9596
restoreThreadContext(context, oldValue)
9697
}
9798
}
9899

100+
internal actual inline fun <T> withThreadLocalContext(context: CoroutineContext, block: () -> T) : T {
101+
val old = currentContextThreadLocal.get()
102+
currentContextThreadLocal.set(context)
103+
try {
104+
return block()
105+
} finally {
106+
currentContextThreadLocal.set(old)
107+
}
108+
}
109+
99110
/**
100111
* Executes a block using a context of a given continuation.
101112
*/
@@ -109,7 +120,7 @@ internal actual inline fun <T> withContinuationContext(continuation: Continuatio
109120
null // fast path -- don't even try to find undispatchedCompletion as there's nothing to restore in the context
110121
}
111122
try {
112-
return block()
123+
return withThreadLocalContext(context, block)
113124
} finally {
114125
if (undispatchedCompletion == null || undispatchedCompletion.clearThreadContext()) {
115126
restoreThreadContext(context, oldValue)

kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ internal actual class SafeCollector<T> actual constructor(
5757

5858
override fun invokeSuspend(result: Result<Any?>): Any {
5959
result.onFailure { lastEmissionContext = DownstreamExceptionContext(it, context) }
60-
completion_?.resumeWith(result as Result<Unit>)
60+
withThreadLocalContext(context) {
61+
completion_?.resumeWith(result as Result<Unit>)
62+
}
6163
return COROUTINE_SUSPENDED
6264
}
6365

kotlinx-coroutines-core/jvm/src/internal/intellij/intellij.kt

+16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
/**
2+
* A special file that contains IntelliJ-related functions
3+
*/
14
package kotlinx.coroutines.internal.intellij
25

36
import kotlinx.coroutines.InternalCoroutinesApi
7+
import kotlin.coroutines.*
8+
9+
internal val currentContextThreadLocal : ThreadLocal<CoroutineContext?> = ThreadLocal.withInitial { null }
410

511
/**
612
* [IntellijCoroutines] exposes the API added as part of IntelliJ patches.
@@ -9,4 +15,14 @@ import kotlinx.coroutines.InternalCoroutinesApi
915
@InternalCoroutinesApi
1016
public object IntellijCoroutines {
1117

18+
/**
19+
* IntelliJ Platform would like to introspect coroutine contexts outside the coroutine framework.
20+
* This function is a non-suspend version of [coroutineContext].
21+
*
22+
* @return null if current thread is not used by coroutine dispatchers,
23+
* or [coroutineContext] otherwise.
24+
*/
25+
public fun currentThreadCoroutineContext(): CoroutineContext? {
26+
return currentContextThreadLocal.get()
27+
}
1228
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package kotlinx.coroutines
2+
3+
import kotlinx.coroutines.flow.*
4+
import kotlinx.coroutines.internal.intellij.*
5+
import kotlinx.coroutines.testing.*
6+
import org.junit.Assert.*
7+
import org.junit.Test
8+
import kotlin.coroutines.*
9+
10+
class ExposedThreadContextTest : TestBase() {
11+
@Test
12+
fun runBlocking() = runBlocking {
13+
assertContextEqualUnderResumption()
14+
}
15+
16+
class C : AbstractCoroutineContextElement(C) {
17+
companion object Key : CoroutineContext.Key<C>
18+
}
19+
20+
@Test
21+
fun withContext() = runBlocking {
22+
val element = C()
23+
withContext(element) {
24+
// the context changed
25+
assertContextEqualUnderResumption()
26+
withContext(element) {
27+
// checking fast path -- the context effectively does not change
28+
assertContextEqualUnderResumption()
29+
}
30+
}
31+
}
32+
33+
@Test
34+
fun launch() = runBlocking {
35+
for (i in 0..10) {
36+
launch {
37+
assertContextEqualUnderResumption()
38+
}
39+
}
40+
}
41+
42+
@Test
43+
fun coroutineScope() = runBlocking {
44+
coroutineScope {
45+
assertContextEqualUnderResumption()
46+
coroutineScope {
47+
assertContextEqualUnderResumption()
48+
}
49+
}
50+
}
51+
52+
@Test
53+
fun supervisorScope() = runBlocking {
54+
supervisorScope {
55+
assertContextEqualUnderResumption()
56+
supervisorScope {
57+
assertContextEqualUnderResumption()
58+
}
59+
}
60+
}
61+
62+
@Test
63+
fun testFlow() = runBlocking {
64+
coroutineScope {
65+
val flowVar = flow {
66+
repeat(10) {
67+
// Flow has encapsulated context
68+
assertContextEqualUnderResumption()
69+
emit(it)
70+
}
71+
}.flowOn(C())
72+
flowVar.collect {
73+
assertContextEqualUnderResumption()
74+
}
75+
}
76+
}
77+
78+
private suspend fun assertContextEqualUnderResumption() {
79+
// thread context should survive dispatches
80+
assertContextsEqual()
81+
yield()
82+
assertContextsEqual()
83+
}
84+
85+
86+
private suspend fun assertContextsEqual() {
87+
val coroutineContext = currentCoroutineContext()
88+
val threadContext = IntellijCoroutines.currentThreadCoroutineContext()
89+
assertEquals(coroutineContext, threadContext)
90+
}
91+
}

kotlinx-coroutines-core/native/src/CoroutineContext.kt

+2
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ internal actual class UndispatchedCoroutine<in T> actual constructor(
5151
) : ScopeCoroutine<T>(context, uCont) {
5252
override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont))
5353
}
54+
55+
internal actual inline fun <T> withThreadLocalContext(context: CoroutineContext, block: () -> T) : T = block()

kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt

+2
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ internal actual class UndispatchedCoroutine<in T> actual constructor(
4545
) : ScopeCoroutine<T>(context, uCont) {
4646
override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont))
4747
}
48+
49+
internal actual inline fun <T> withThreadLocalContext(context: CoroutineContext, block: () -> T) : T = block()

0 commit comments

Comments
 (0)