From 4fb424c3e7b4cdee9112a55ca5a782b729c3ff76 Mon Sep 17 00:00:00 2001 From: Patrick Jackson Date: Sat, 22 Feb 2020 18:48:04 -0500 Subject: [PATCH 1/2] bugfix issue #38 enforce same thread --- buildSrc/src/main/kotlin/Libs.kt | 6 + buildSrc/src/main/kotlin/Versions.kt | 1 + lib/build.gradle | 2 + .../kotlin/org/reduxkotlin/CreateStore.kt | 5 +- .../org/reduxkotlin/utils/ThreadUtil.kt | 12 +- .../org/reduxkotlin/util/ThreadUtilSpec.kt | 114 +++++++++++++++++- 6 files changed, 131 insertions(+), 9 deletions(-) diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 2ef367c..785c346 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -180,4 +180,10 @@ object Libs { const val spek_runner_junit5: String = "org.spekframework.spek2:spek-runner-junit5:" + Versions.spek + + const val kotlin_coroutines_jvm: String = "org.jetbrains.kotlinx:kotlinx-coroutines-core:" + + Versions.coroutines + + const val kotlin_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" + + Versions.coroutines } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index bdce89d..8454e96 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -43,6 +43,7 @@ object Versions { const val recycler_view = "1.0.0" + const val coroutines = "1.3.3" /** * diff --git a/lib/build.gradle b/lib/build.gradle index f267f8b..70f93b0 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -59,6 +59,8 @@ kotlin { dependencies { implementation kotlin("test") implementation kotlin("test-junit") + implementation Libs.kotlin_coroutines_test + implementation Libs.kotlin_coroutines_jvm implementation Libs.spek_dsl_jvm implementation Libs.atrium_cc_en_gb_robstoll implementation Libs.mockk diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt index 79400b2..42ef4ba 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt @@ -2,6 +2,7 @@ package org.reduxkotlin import org.reduxkotlin.utils.getThreadName import org.reduxkotlin.utils.isPlainObject +import org.reduxkotlin.utils.stripCoroutineName /** * Creates a Redux store that holds the state tree. @@ -45,8 +46,8 @@ fun createStore( var currentListeners = mutableListOf<() -> Unit>() var nextListeners = currentListeners var isDispatching = false - val storeThreadName = getThreadName() - fun isSameThread() = getThreadName() == storeThreadName + val storeThreadName = stripCoroutineName(getThreadName()) + fun isSameThread() = stripCoroutineName(getThreadName()) == storeThreadName fun checkSameThread() = check(isSameThread()) { """You may not call the store from a thread other than the thread on which it was created. |This includes: getState(), dispatch(), subscribe(), and replaceReducer() diff --git a/lib/src/commonMain/kotlin/org/reduxkotlin/utils/ThreadUtil.kt b/lib/src/commonMain/kotlin/org/reduxkotlin/utils/ThreadUtil.kt index 25dedc3..700a6da 100644 --- a/lib/src/commonMain/kotlin/org/reduxkotlin/utils/ThreadUtil.kt +++ b/lib/src/commonMain/kotlin/org/reduxkotlin/utils/ThreadUtil.kt @@ -5,4 +5,14 @@ const val UNKNOWN_THREAD_NAME = "UNKNOWN_THREAD_NAME" /** * Returns the name of the current thread. */ -expect fun getThreadName(): String \ No newline at end of file +expect fun getThreadName(): String + +/** + * Thread name may have '@coroutine#n' appended to it. + * This strips the suffix so we can compare threads. + */ +fun stripCoroutineName(threadName: String): String { + val lastIndex = threadName.lastIndexOf('@') + return if (lastIndex < 0) threadName + else threadName.substring(0, lastIndex) +} diff --git a/lib/src/jvmTest/kotlin/org/reduxkotlin/util/ThreadUtilSpec.kt b/lib/src/jvmTest/kotlin/org/reduxkotlin/util/ThreadUtilSpec.kt index 0290261..5804eae 100644 --- a/lib/src/jvmTest/kotlin/org/reduxkotlin/util/ThreadUtilSpec.kt +++ b/lib/src/jvmTest/kotlin/org/reduxkotlin/util/ThreadUtilSpec.kt @@ -1,14 +1,23 @@ package org.reduxkotlin.util +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.setMain import org.reduxkotlin.* import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors import kotlin.IllegalStateException -import kotlin.test.assertNotNull -import kotlin.test.assertNull +import kotlin.system.measureTimeMillis +import kotlin.test.* + object ThreadUtilSpec : Spek({ + val mainThreadSurrogate = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + Dispatchers.setMain(mainThreadSurrogate) + describe("createStore") { val store = createStore( todos, TestState( @@ -28,21 +37,75 @@ object ThreadUtilSpec : Spek({ ensureSameThread { store.dispatch(Any()) } } it("ensure same thread on replaceReducer") { - ensureSameThread { store.replaceReducer { state, action -> state } } + ensureSameThread { store.replaceReducer { state, action -> state } } } it("ensure same thread on subscribe") { ensureSameThread { store.subscribe { } } } + it("enforces same thread when thread name appends coroutine name") { + val middleware = TestMiddleware() + + runBlocking { + CoroutineScope(Dispatchers.Main).async { + val store = createStore( + testReducer, + TestState(), + applyMiddleware(middleware.middleware) + ) + + store.dispatch(Any()) + }.await() + Thread.sleep(2000) + assertFalse(middleware.failed) + } + } + it("increments massively") { + suspend fun massiveRun(action: suspend () -> Unit) { + val n = 100 // number of coroutines to launch + val k = 1000 // times an action is repeated by each coroutine + val time = measureTimeMillis { + coroutineScope { + // scope for coroutines + repeat(n) { + launch { + repeat(k) { action() } + } + } + } + } + println("Completed ${n * k} actions in $time ms") + } + + + val counterContext = newSingleThreadContext("CounterContext") + + lateinit var store: Store + runBlocking { + withContext(counterContext) { + store = createStore(counterReducer, TestCounterState()) + } + } + runBlocking { + withContext(counterContext) { + massiveRun { + store.dispatch(Increment()) + } + } + withContext(counterContext) { + assertEquals(100000, store.state.counter) + } + } + } } }) -private fun ensureSameThread(getState: () -> Any) { +private fun ensureSameThread(testFun: () -> Any) { val latch = CountDownLatch(1) var exception: java.lang.IllegalStateException? = null var state: Any? = null val newThread = Thread { - state = getState() + state = testFun() } newThread.setUncaughtExceptionHandler { thread, throwable -> @@ -55,4 +118,43 @@ private fun ensureSameThread(getState: () -> Any) { assertNotNull(exception) assertNull(state) -} \ No newline at end of file +} + +val testReducer: Reducer = { state, action -> state } + +/** + * Used as a test for when Thread.currentThread.name returns the + * thread name + '@coroutine#'. + * See issue #38 https://github.com/reduxkotlin/redux-kotlin/issues/38 + */ +class TestMiddleware { + var failed = false + val middleware = middleware { store, next, action -> + CoroutineScope(Dispatchers.Main).launch { + flow { + delay(1000) // simulate api call + emit("Text Response") + }.collect { response -> + store.dispatch("") + } + } + try { + next(action) + } catch (e: Exception) { + e.printStackTrace() + failed = true + Unit + } + } +} + +class Increment + +data class TestCounterState(val counter: Int = 0) + +val counterReducer = { state: TestCounterState, action: Any -> + when (action) { + is Increment -> state.copy(counter = state.counter + 1) + else -> state + } +} From ac34b4130460b9575d316fa252edb46960baa3c4 Mon Sep 17 00:00:00 2001 From: Patrick Jackson Date: Sat, 22 Feb 2020 20:05:39 -0500 Subject: [PATCH 2/2] bump to v3.2 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 70f93b0..548b800 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'kotlin-multiplatform' archivesBaseName = 'redux-kotlin' group 'org.reduxkotlin' -version '0.3.1' +version '0.3.2' kotlin { jvm()