Skip to content

Commit 9f9d5f4

Browse files
committed
createSameThreadEnforcedStore & createThreadSafeStore
1 parent d271835 commit 9f9d5f4

File tree

12 files changed

+323
-23
lines changed

12 files changed

+323
-23
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ buildscript {
1111
classpath Plugins.kotlin
1212
classpath Plugins.dokka
1313
classpath Plugins.android
14+
classpath Plugins.atomicFu
1415
}
1516
}
1617

@@ -24,7 +25,6 @@ allprojects {
2425
jcenter()
2526
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
2627
maven { url "https://dl.bintray.com/spekframework/spek-dev" }
27-
2828
}
2929

3030
group = GROUP

buildSrc/src/main/kotlin/Libs.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ object Libs {
181181
const val spek_runner_junit5: String = "org.spekframework.spek2:spek-runner-junit5:" +
182182
Versions.spek
183183

184+
const val kotlin_coroutines: String = "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:" +
185+
Versions.coroutines
186+
184187
const val kotlin_coroutines_jvm: String = "org.jetbrains.kotlinx:kotlinx-coroutines-core:" +
185188
Versions.coroutines
186189

buildSrc/src/main/kotlin/Plugins.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ object Plugins {
22
const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.org_jetbrains_kotlin}"
33
const val dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${Versions.dokka_gradle_plugin}"
44
const val android = "com.android.tools.build:gradle:${Versions.com_android_tools_build_gradle}"
5-
}
5+
const val atomicFu = "org.jetbrains.kotlinx:atomicfu-gradle-plugin:${Versions.atomicFu}"
6+
}

buildSrc/src/main/kotlin/Versions.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ object Versions {
4545

4646
const val coroutines = "1.3.7"
4747

48+
const val atomicFu = "0.14.2"
49+
4850
/**
4951
*
5052
* See issue 19: How to update Gradle itself?

lib-threadsafe/build.gradle

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
repositories {
2+
maven { url "https://dl.bintray.com/spekframework/spek-dev" }
3+
}
4+
apply plugin: 'java'
5+
apply plugin: 'kotlin-multiplatform'
6+
apply plugin: 'kotlinx-atomicfu'
7+
8+
archivesBaseName = 'redux-kotlin-threadsafe'
9+
10+
group 'org.reduxkotlin'
11+
version '0.4.0'
12+
13+
kotlin {
14+
jvm()
15+
js() {
16+
[compileKotlinJs, compileTestKotlinJs].each { configuration ->
17+
configuration.kotlinOptions {
18+
moduleKind = 'umd'
19+
sourceMap = true
20+
metaInfo = true
21+
}
22+
}
23+
}
24+
25+
iosArm64("ios")
26+
iosX64("iosSim")
27+
macosX64("macos")
28+
mingwX64("win")
29+
wasm32("wasm")
30+
linuxArm32Hfp("linArm32")
31+
linuxMips32("linMips32")
32+
linuxMipsel32("linMipsel32")
33+
linuxX64("lin64")
34+
35+
sourceSets {
36+
37+
commonMain {
38+
dependencies {
39+
implementation kotlin("stdlib-common")
40+
implementation project(":lib")
41+
}
42+
}
43+
commonTest {
44+
kotlin.srcDir('src/test/kotlin')
45+
dependencies {
46+
implementation kotlin("test-common")
47+
implementation kotlin("test-annotations-common")
48+
implementation Libs.spek_dsl_metadata
49+
implementation Libs.atrium_cc_en_gb_robstoll_common
50+
implementation Libs.mockk_common
51+
implementation Libs.kotlin_coroutines
52+
}
53+
}
54+
55+
jvmMain {
56+
kotlin.srcDir('src/jvmMain/kotlin')
57+
dependencies {
58+
implementation kotlin("stdlib")
59+
}
60+
}
61+
jvmTest {
62+
dependencies {
63+
implementation kotlin("test")
64+
implementation kotlin("test-junit")
65+
implementation Libs.kotlin_coroutines_test
66+
implementation Libs.kotlin_coroutines_jvm
67+
implementation Libs.spek_dsl_jvm
68+
implementation Libs.atrium_cc_en_gb_robstoll
69+
implementation Libs.mockk
70+
71+
runtimeOnly Libs.spek_runner_junit5
72+
runtimeOnly Libs.kotlin_reflect
73+
74+
}
75+
}
76+
jsMain {
77+
kotlin.srcDir('src/jsMain/kotlin')
78+
dependencies {
79+
implementation kotlin("stdlib-js")
80+
}
81+
compileKotlinJs {
82+
kotlinOptions.metaInfo = true
83+
kotlinOptions.sourceMap = true
84+
kotlinOptions.suppressWarnings = true
85+
kotlinOptions.verbose = true
86+
kotlinOptions.main = "call"
87+
kotlinOptions.moduleKind = "umd"
88+
}
89+
}
90+
jsTest {
91+
dependencies {
92+
implementation kotlin("test-js")
93+
implementation kotlin("stdlib-js")
94+
}
95+
}
96+
97+
iosSimMain.dependsOn iosMain
98+
iosSimTest.dependsOn iosTest
99+
}
100+
}
101+
102+
103+
afterEvaluate {
104+
// Alias the task names we use elsewhere to the new task names.
105+
tasks.create('installMP').dependsOn('publishKotlinMultiplatformPublicationToMavenLocal')
106+
tasks.create('installLocally') {
107+
dependsOn 'publishKotlinMultiplatformPublicationToTestRepository'
108+
dependsOn 'publishJvmPublicationToTestRepository'
109+
dependsOn 'publishJsPublicationToTestRepository'
110+
dependsOn 'publishMetadataPublicationToTestRepository'
111+
}
112+
tasks.create('installIosLocally') {
113+
dependsOn 'publishKotlinMultiplatformPublicationToTestRepository'
114+
dependsOn 'publishIosArm32PublicationToTestRepository'
115+
dependsOn 'publishIosArm64PublicationToTestRepository'
116+
dependsOn 'publishIosX64PublicationToTestRepository'
117+
dependsOn 'publishMetadataPublicationToTestRepository'
118+
}
119+
// NOTE: We do not alias uploadArchives because CI runs it on Linux and we only want to run it on Mac OS.
120+
//tasks.create('uploadArchives').dependsOn('publishKotlinMultiplatformPublicationToMavenRepository')
121+
}
122+
123+
apply from: rootProject.file('gradle/publish.gradle')
124+
125+
publishing {
126+
publications.all {
127+
// Rewrite all artifacts from using the project name to just 'runtime'.
128+
artifactId = artifactId.replace(project.name, 'redux-kotlin-threadsafe')
129+
}
130+
}
131+
132+
jvmTest {
133+
useJUnitPlatform {
134+
includeEngines 'spek2'
135+
}
136+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.reduxkotlin
2+
3+
/**
4+
* Creates a SYNCHRONIZED, THREADSAFE Redux store that holds the state tree.
5+
* The only way to change the data in the store is to call `dispatch()` on it.
6+
*
7+
* There should only be a single store in your app. To specify how different
8+
* parts of the state tree respond to actions, you may combine several reducers
9+
* into a single reducer function by using `combineReducers`.
10+
*
11+
* @param {Reducer} [reducer] A function that returns the next state tree, given
12+
* the current state tree and the action to handle.
13+
*
14+
* @param {Any} [preloadedState] The initial state. You may optionally specify
15+
* it to hydrate the state from the server in universal apps, or to restore a
16+
* previously serialized user session.
17+
*
18+
* @param {Enhancer} [enhancer] The store enhancer. You may optionally specify
19+
* it to enhance the store with third-party capabilities such as middleware,
20+
* time travel, persistence, etc. The only store enhancer that ships with Redux
21+
* is `applyMiddleware()`.
22+
*
23+
* @returns {Store} A Redux store that lets you read the state, dispatch actions
24+
* and subscribe to changes.
25+
*/
26+
fun <State> createThreadSafeStore(
27+
reducer: Reducer<State>,
28+
preloadedState: State,
29+
enhancer: StoreEnhancer<State>? = null
30+
): Store<State> = SynchronizedStore(createStore(reducer, preloadedState, enhancer))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.reduxkotlin
2+
3+
import kotlinx.atomicfu.locks.SynchronizedObject
4+
import kotlinx.atomicfu.locks.synchronized
5+
6+
/**
7+
* Threadsafe wrapper for ReduxKotlin store that synchronizes access to each function using
8+
* kotlinx.AtomicFu [https://github.com/Kotlin/kotlinx.atomicfu]
9+
* Allows all store functions to be accessed from any thread.
10+
* This does have a performance impact for JVM/Native.
11+
* TODO more info at [https://ReduxKotlin.org]
12+
*/
13+
class SynchronizedStore<TState>(private val store: Store<TState>) : Store<TState>, SynchronizedObject() {
14+
15+
override var dispatch: Dispatcher = { action ->
16+
synchronized(this) { store.dispatch(action) }
17+
}
18+
19+
override val getState: GetState<TState> = {
20+
synchronized(this) { store.getState() }
21+
}
22+
23+
override val replaceReducer: (Reducer<TState>) -> Unit = { reducer ->
24+
synchronized(this) { store.replaceReducer(reducer) }
25+
}
26+
27+
override val subscribe: (StoreSubscriber) -> StoreSubscription = { storeSubscriber ->
28+
synchronized(this) { store.subscribe(storeSubscriber) }
29+
}
30+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.reduxkotlin.util
2+
3+
import kotlinx.coroutines.*
4+
import org.reduxkotlin.*
5+
import org.spekframework.spek2.Spek
6+
import org.spekframework.spek2.style.specification.describe
7+
import kotlin.system.measureTimeMillis
8+
import kotlin.test.*
9+
10+
object CreateThreadSafeStoreSpec : Spek({
11+
describe("createThreadSafeStore") {
12+
it("multithreaded increments massively") {
13+
suspend fun massiveRun(action: suspend () -> Unit) {
14+
val n = 100 // number of coroutines to launch
15+
val k = 1000 // times an action is repeated by each coroutine
16+
val time = measureTimeMillis {
17+
coroutineScope {
18+
// scope for coroutines
19+
repeat(n) {
20+
launch {
21+
repeat(k) { action() }
22+
}
23+
}
24+
}
25+
}
26+
println("Completed ${n * k} actions in $time ms")
27+
}
28+
29+
val counterContext = newFixedThreadPoolContext(5, "CounterContext")
30+
31+
//NOTE: changing this to createStore() breaks the tests
32+
val store = createThreadSafeStore(counterReducer, TestCounterState())
33+
runBlocking {
34+
withContext(counterContext) {
35+
massiveRun {
36+
store.dispatch(Increment())
37+
}
38+
}
39+
withContext(counterContext) {
40+
assertEquals(100000, store.state.counter)
41+
}
42+
}
43+
}
44+
}
45+
})
46+
47+
class Increment
48+
49+
data class TestCounterState(val counter: Int = 0)
50+
51+
val counterReducer = { state: TestCounterState, action: Any ->
52+
when (action) {
53+
is Increment -> state.copy(counter = state.counter + 1)
54+
else -> state
55+
}
56+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.reduxkotlin
2+
3+
import org.reduxkotlin.utils.getThreadName
4+
import org.reduxkotlin.utils.stripCoroutineName
5+
6+
/**
7+
* Creates a Redux store that can only be accessed from the same thread.
8+
* Any call to the store's functions called from a thread other than thread
9+
* from which it was created will throw an IllegalStateException.
10+
*
11+
* Most use cases will want to use [createThreadSafeStore] or [createStore]
12+
* More details: TODO add documentation link
13+
*
14+
* see [createStore] for details on store params/behavior
15+
*/
16+
fun <State> createSameThreadEnforcedStore(
17+
reducer: Reducer<State>,
18+
preloadedState: State,
19+
enhancer: StoreEnhancer<State>? = null
20+
): Store<State> {
21+
22+
val store = createStore(reducer, preloadedState, enhancer)
23+
val storeThreadName = stripCoroutineName(getThreadName())
24+
fun isSameThread() = stripCoroutineName(getThreadName()) == storeThreadName
25+
fun checkSameThread() = check(isSameThread()) {
26+
"""You may not call the store from a thread other than the thread on which it was created.
27+
|This includes: getState(), dispatch(), subscribe(), and replaceReducer()
28+
|This store was created on: '$storeThreadName' and current
29+
|thread is '${getThreadName()}'
30+
""".trimMargin()
31+
}
32+
33+
return object : Store<State> {
34+
override val getState = {
35+
checkSameThread()
36+
store.getState()
37+
}
38+
39+
override var dispatch: Dispatcher = { action ->
40+
checkSameThread()
41+
store.dispatch(action)
42+
}
43+
44+
override val subscribe = { storeSubscriber: StoreSubscriber ->
45+
checkSameThread()
46+
store.subscribe(storeSubscriber)
47+
}
48+
49+
override val replaceReducer = { reducer: Reducer<State> ->
50+
checkSameThread()
51+
store.replaceReducer(reducer)
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)