Skip to content

Commit d99f721

Browse files
committed
bugfix issue #38 enforce same thread
1 parent 038fce1 commit d99f721

File tree

6 files changed

+133
-9
lines changed

6 files changed

+133
-9
lines changed

buildSrc/src/main/kotlin/Libs.kt

+6
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,10 @@ object Libs {
180180

181181
const val spek_runner_junit5: String = "org.spekframework.spek2:spek-runner-junit5:" +
182182
Versions.spek
183+
184+
const val kotlin_coroutines_jvm: String = "org.jetbrains.kotlinx:kotlinx-coroutines-core:" +
185+
Versions.coroutines
186+
187+
const val kotlin_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" +
188+
Versions.coroutines
183189
}

buildSrc/src/main/kotlin/Versions.kt

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ object Versions {
4343

4444
const val recycler_view = "1.0.0"
4545

46+
const val coroutines = "1.3.3"
4647

4748
/**
4849
*

lib/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ kotlin {
5959
dependencies {
6060
implementation kotlin("test")
6161
implementation kotlin("test-junit")
62+
implementation Libs.kotlin_coroutines_test
63+
implementation Libs.kotlin_coroutines_jvm
6264
implementation Libs.spek_dsl_jvm
6365
implementation Libs.atrium_cc_en_gb_robstoll
6466
implementation Libs.mockk

lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.reduxkotlin
22

33
import org.reduxkotlin.utils.getThreadName
44
import org.reduxkotlin.utils.isPlainObject
5+
import org.reduxkotlin.utils.stripCoroutineName
56

67
/**
78
* Creates a Redux store that holds the state tree.
@@ -45,8 +46,8 @@ fun <State> createStore(
4546
var currentListeners = mutableListOf<() -> Unit>()
4647
var nextListeners = currentListeners
4748
var isDispatching = false
48-
val storeThreadName = getThreadName()
49-
fun isSameThread() = getThreadName() == storeThreadName
49+
val storeThreadName = stripCoroutineName(getThreadName())
50+
fun isSameThread() = stripCoroutineName(getThreadName()) == storeThreadName
5051
fun checkSameThread() = check(isSameThread()) {
5152
"""You may not call the store from a thread other than the thread on which it was created.
5253
|This includes: getState(), dispatch(), subscribe(), and replaceReducer()

lib/src/commonMain/kotlin/org/reduxkotlin/utils/ThreadUtil.kt

+13-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,16 @@ const val UNKNOWN_THREAD_NAME = "UNKNOWN_THREAD_NAME"
55
/**
66
* Returns the name of the current thread.
77
*/
8-
expect fun getThreadName(): String
8+
expect fun getThreadName(): String
9+
10+
/**
11+
* Thread name may have '@coroutine#n' appended to it.
12+
* This strips the suffix so we can compare threads.
13+
*/
14+
fun stripCoroutineName(threadName: String): String {
15+
// return threadName
16+
val lastIndex = threadName.lastIndexOf('@')
17+
return if (lastIndex < 0) threadName
18+
else
19+
threadName.substring(0, lastIndex)
20+
}

lib/src/jvmTest/kotlin/org/reduxkotlin/util/ThreadUtilSpec.kt

+108-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package org.reduxkotlin.util
22

3+
import kotlinx.coroutines.*
4+
import kotlinx.coroutines.flow.collect
5+
import kotlinx.coroutines.flow.flow
6+
import kotlinx.coroutines.test.setMain
37
import org.reduxkotlin.*
48
import org.spekframework.spek2.Spek
59
import org.spekframework.spek2.style.specification.describe
610
import java.util.concurrent.CountDownLatch
11+
import java.util.concurrent.Executors
712
import kotlin.IllegalStateException
8-
import kotlin.test.assertNotNull
9-
import kotlin.test.assertNull
13+
import kotlin.system.measureTimeMillis
14+
import kotlin.test.*
15+
1016

1117
object ThreadUtilSpec : Spek({
18+
val mainThreadSurrogate = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
19+
Dispatchers.setMain(mainThreadSurrogate)
20+
1221
describe("createStore") {
1322
val store = createStore(
1423
todos, TestState(
@@ -28,21 +37,75 @@ object ThreadUtilSpec : Spek({
2837
ensureSameThread { store.dispatch(Any()) }
2938
}
3039
it("ensure same thread on replaceReducer") {
31-
ensureSameThread { store.replaceReducer { state, action -> state } }
40+
ensureSameThread { store.replaceReducer { state, action -> state } }
3241
}
3342
it("ensure same thread on subscribe") {
3443
ensureSameThread { store.subscribe { } }
3544
}
45+
it("enforces same thread when thread name appends coroutine name") {
46+
val middleware = TestMiddleware()
47+
48+
runBlocking {
49+
CoroutineScope(Dispatchers.Main).async {
50+
val store = createStore(
51+
testReducer,
52+
TestState(),
53+
applyMiddleware(middleware.middleware)
54+
)
55+
56+
store.dispatch(Any())
57+
}.await()
58+
Thread.sleep(2000)
59+
assertFalse(middleware.failed)
60+
}
61+
}
62+
it("increments massively") {
63+
suspend fun massiveRun(action: suspend () -> Unit) {
64+
val n = 100 // number of coroutines to launch
65+
val k = 1000 // times an action is repeated by each coroutine
66+
val time = measureTimeMillis {
67+
coroutineScope {
68+
// scope for coroutines
69+
repeat(n) {
70+
launch {
71+
repeat(k) { action() }
72+
}
73+
}
74+
}
75+
}
76+
println("Completed ${n * k} actions in $time ms")
77+
}
78+
79+
80+
val counterContext = newSingleThreadContext("CounterContext")
81+
82+
lateinit var store: Store<TestCounterState>
83+
runBlocking {
84+
withContext(counterContext) {
85+
store = createStore(counterReducer, TestCounterState())
86+
}
87+
}
88+
runBlocking {
89+
withContext(counterContext) {
90+
massiveRun {
91+
store.dispatch(Increment())
92+
}
93+
}
94+
withContext(counterContext) {
95+
assertEquals(100000, store.state.counter)
96+
}
97+
}
98+
}
3699
}
37100
})
38101

39-
private fun ensureSameThread(getState: () -> Any) {
102+
private fun ensureSameThread(testFun: () -> Any) {
40103
val latch = CountDownLatch(1)
41104
var exception: java.lang.IllegalStateException? = null
42105
var state: Any? = null
43106

44107
val newThread = Thread {
45-
state = getState()
108+
state = testFun()
46109
}
47110

48111
newThread.setUncaughtExceptionHandler { thread, throwable ->
@@ -55,4 +118,43 @@ private fun ensureSameThread(getState: () -> Any) {
55118

56119
assertNotNull(exception)
57120
assertNull(state)
58-
}
121+
}
122+
123+
val testReducer: Reducer<TestState> = { state, action -> state }
124+
125+
/**
126+
* Used as a test for when Thread.currentThread.name returns the
127+
* thread name + '@coroutine#'.
128+
* See issue #38 https://github.com/reduxkotlin/redux-kotlin/issues/38
129+
*/
130+
class TestMiddleware {
131+
var failed = false
132+
val middleware = middleware<TestState> { store, next, action ->
133+
CoroutineScope(Dispatchers.Main).launch {
134+
flow {
135+
delay(1000) // simulate api call
136+
emit("Text Response")
137+
}.collect { response ->
138+
store.dispatch("")
139+
}
140+
}
141+
try {
142+
next(action)
143+
} catch (e: Exception) {
144+
e.printStackTrace()
145+
failed = true
146+
Unit
147+
}
148+
}
149+
}
150+
151+
class Increment
152+
153+
data class TestCounterState(val counter: Int = 0)
154+
155+
val counterReducer = { state: TestCounterState, action: Any ->
156+
when (action) {
157+
is Increment -> state.copy(counter = state.counter + 1)
158+
else -> state
159+
}
160+
}

0 commit comments

Comments
 (0)