Skip to content

Commit 1074e33

Browse files
Copy CopyableThreadContextElement when switching context with flowOn (#3778)
`flowOn` uses its own undispatched coroutine start when it detects a fast path. Previously, it concatenated the context missing the copy of CopyableThreadContextElement. Fixed by replacing concatenation with `newCoroutineContext`. Fixes #3787 Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
1 parent f1404c0 commit 1074e33

File tree

3 files changed

+48
-2
lines changed

3 files changed

+48
-2
lines changed

kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ internal abstract class ChannelFlowOperator<S, T>(
161161
// Fast-path: When channel creation is optional (flowOn/flowWith operators without buffer)
162162
if (capacity == Channel.OPTIONAL_CHANNEL) {
163163
val collectContext = coroutineContext
164-
val newContext = collectContext + context // compute resulting collect context
164+
val newContext = collectContext.newCoroutineContext(context) // compute resulting collect context
165165
// #1: If the resulting context happens to be the same as it was -- fallback to plain collect
166166
if (newContext == collectContext)
167167
return flowCollect(collector)

kotlinx-coroutines-core/jvm/test/ThreadContextElementTest.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package kotlinx.coroutines
77
import org.junit.Test
88
import kotlin.coroutines.*
99
import kotlin.test.*
10+
import kotlinx.coroutines.flow.*
1011

1112
class ThreadContextElementTest : TestBase() {
1213

@@ -37,7 +38,7 @@ class ThreadContextElementTest : TestBase() {
3738
}
3839

3940
@Test
40-
fun testUndispatched()= runTest {
41+
fun testUndispatched() = runTest {
4142
val exceptionHandler = coroutineContext[CoroutineExceptionHandler]!!
4243
val data = MyData()
4344
val element = MyElement(data)
@@ -191,6 +192,21 @@ class ThreadContextElementTest : TestBase() {
191192

192193
assertEquals(manuallyCaptured, captor.capturees)
193194
}
195+
196+
@Test
197+
fun testThreadLocalFlowOn() = runTest {
198+
val myData = MyData()
199+
myThreadLocal.set(myData)
200+
expect(1)
201+
flow {
202+
assertEquals(myData, myThreadLocal.get())
203+
emit(1)
204+
}
205+
.flowOn(myThreadLocal.asContextElement() + Dispatchers.Default)
206+
.single()
207+
myThreadLocal.set(null)
208+
finish(2)
209+
}
194210
}
195211

196212
class MyData
@@ -259,6 +275,7 @@ class CopyForChildCoroutineElement(val data: MyData?) : CopyableThreadContextEle
259275
}
260276
}
261277

278+
262279
/**
263280
* Calls [block], setting the value of [this] [ThreadLocal] for the duration of [block].
264281
*

kotlinx-coroutines-core/jvm/test/ThreadContextMutableCopiesTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.coroutines
66

7+
import kotlinx.coroutines.flow.*
78
import kotlin.coroutines.*
89
import kotlin.test.*
910

@@ -131,4 +132,32 @@ class ThreadContextMutableCopiesTest : TestBase() {
131132
finish(2)
132133
}
133134
}
135+
136+
@Test
137+
fun testDataIsCopiedThroughFlowOnUndispatched() = runTest {
138+
expect(1)
139+
val root = MyMutableElement(ArrayList())
140+
val originalData = root.mutableData
141+
flow {
142+
assertNotSame(originalData, threadLocalData.get())
143+
emit(1)
144+
}
145+
.flowOn(root)
146+
.single()
147+
finish(2)
148+
}
149+
150+
@Test
151+
fun testDataIsCopiedThroughFlowOnDispatched() = runTest {
152+
expect(1)
153+
val root = MyMutableElement(ArrayList())
154+
val originalData = root.mutableData
155+
flow {
156+
assertNotSame(originalData, threadLocalData.get())
157+
emit(1)
158+
}
159+
.flowOn(root + Dispatchers.Default)
160+
.single()
161+
finish(2)
162+
}
134163
}

0 commit comments

Comments
 (0)