Skip to content

Commit 8e92214

Browse files
committed
Throw if using Dispatchers.Default or Dispatchers.IO
1 parent bb35577 commit 8e92214

4 files changed

Lines changed: 67 additions & 10 deletions

File tree

kstatemachine-coroutines/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/CoroutinesStateMachine.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ru.nsk.kstatemachine.state.ChildMode
1515
import kotlin.contracts.ExperimentalContracts
1616
import kotlin.contracts.InvocationKind
1717
import kotlin.contracts.contract
18+
import kotlin.coroutines.ContinuationInterceptor
1819
import kotlin.coroutines.CoroutineContext
1920
import kotlin.coroutines.EmptyCoroutineContext
2021

@@ -42,10 +43,31 @@ suspend fun createStateMachine(
4243
contract {
4344
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
4445
}
46+
checkCoroutineScopeValidity(scope, creationArguments)
4547
return CoroutinesLibCoroutineAbstraction(scope)
4648
.createStateMachine(name, childMode, start, creationArguments, init)
4749
}
4850

51+
private fun checkCoroutineScopeValidity(scope: CoroutineScope, creationArguments: CreationArguments) {
52+
if (creationArguments.skipCoroutineScopeValidityCheck) return
53+
54+
val dispatcher = scope.coroutineContext[ContinuationInterceptor]
55+
val dispatcherName = dispatcher.toString()
56+
if (dispatcher === Dispatchers.Default ||
57+
dispatcherName == "Dispatchers.Default" ||
58+
dispatcherName.startsWith("Dispatchers.Default.limitedParallelism") ||
59+
dispatcherName == "Dispatchers.IO" || // can't get IO dispatcher in commonMain
60+
dispatcherName.startsWith("Dispatchers.IO.limitedParallelism")
61+
) {
62+
error(
63+
"Using Dispatchers.Default or Dispatchers.IO for StateMachine even with limitedParallelism(1) is the most likely an error," +
64+
" as it is multi-threaded, see the docs: \n" +
65+
"https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescope" +
66+
"You can opt-out this check by CreationArguments::skipCoroutineScopeValidityCheck flag."
67+
)
68+
}
69+
}
70+
4971
/**
5072
* Processes event in async fashion (using launch() to start new coroutine).
5173
*

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/CreationArguments.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ interface CreationArguments {
4848
* Default: null
4949
*/
5050
val eventRecordingArguments: EventRecordingArguments?
51+
52+
/**
53+
* The library checks if you are trying to use multithreaded Dispatcher like
54+
* Dispatchers.Default or Dispatcher.IO which is usually an error.
55+
* @see [https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescope]
56+
* You can skip this validation setting the flag to true.
57+
* Default: false
58+
*/
59+
val skipCoroutineScopeValidityCheck: Boolean
5160
}
5261

5362
interface CreationArgumentsBuilder : CreationArguments {
@@ -56,14 +65,16 @@ interface CreationArgumentsBuilder : CreationArguments {
5665
override var doNotThrowOnMultipleTransitionsMatch: Boolean
5766
override var requireNonBlankNames: Boolean
5867
override var eventRecordingArguments: EventRecordingArguments?
68+
override var skipCoroutineScopeValidityCheck: Boolean
5969
}
6070

6171
private data class CreationArgumentsBuilderImpl(
6272
override var autoDestroyOnStatesReuse: Boolean = true,
6373
override var isUndoEnabled: Boolean = false,
6474
override var doNotThrowOnMultipleTransitionsMatch: Boolean = false,
6575
override var requireNonBlankNames: Boolean = false,
66-
override var eventRecordingArguments: EventRecordingArguments? = null
76+
override var eventRecordingArguments: EventRecordingArguments? = null,
77+
override var skipCoroutineScopeValidityCheck: Boolean = false,
6778
) : CreationArgumentsBuilder
6879

6980
@OptIn(ExperimentalContracts::class)

tests/src/commonTest/kotlin/ru/nsk/kstatemachine/TestUtils.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ enum class CoroutineStarterType {
7979
* but it should be ok as it happens sequentially.
8080
*/
8181
COROUTINES_LIB_SINGLE_THREAD_DISPATCHER,
82-
COROUTINES_LIB_DEFAULT_LIMITED_DISPATCHER,
8382
}
8483

8584
@OptIn(ExperimentalCoroutinesApi::class)
@@ -132,13 +131,5 @@ suspend fun createTestStateMachine(
132131
creationArguments,
133132
init = init
134133
)
135-
CoroutineStarterType.COROUTINES_LIB_DEFAULT_LIMITED_DISPATCHER -> createStateMachine(
136-
CoroutineScope(Dispatchers.Default.limitedParallelism(1)), // does not guarantee same thread for each task
137-
name,
138-
childMode,
139-
start,
140-
creationArguments,
141-
init = init
142-
)
143134
}
144135
}

tests/src/commonTest/kotlin/ru/nsk/kstatemachine/statemachine/StateMachineTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import io.kotest.matchers.string.shouldEndWith
1919
import io.mockk.called
2020
import io.mockk.verify
2121
import io.mockk.verifySequence
22+
import kotlinx.coroutines.CoroutineScope
23+
import kotlinx.coroutines.Dispatchers
24+
import kotlinx.coroutines.cancel
2225
import ru.nsk.kstatemachine.*
2326
import ru.nsk.kstatemachine.event.Event
2427
import ru.nsk.kstatemachine.event.EventMatcher
@@ -35,6 +38,36 @@ private object StateMachineTestData {
3538
}
3639

3740
class StateMachineTest : FreeSpec({
41+
withData(
42+
nameFn = { "dispatcher: $it" },
43+
CoroutineScope(Dispatchers.Default),
44+
CoroutineScope(Dispatchers.Default.limitedParallelism(1)),
45+
CoroutineScope(Dispatchers.IO),
46+
CoroutineScope(Dispatchers.IO.limitedParallelism(1)),
47+
) { scope ->
48+
"scope validation" {
49+
try {
50+
createStateMachine(
51+
scope,
52+
creationArguments = buildCreationArguments { skipCoroutineScopeValidityCheck = true }
53+
) {
54+
initialState("initial")
55+
}
56+
57+
shouldThrowWithMessage<IllegalStateException>(
58+
"Using Dispatchers.Default or Dispatchers.IO for StateMachine even with limitedParallelism(1) is the most likely an error, as it is multi-threaded, see the docs: \n" +
59+
"https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescopeYou can opt-out this check by CreationArguments::skipCoroutineScopeValidityCheck flag."
60+
) {
61+
createStateMachine(scope) {
62+
initialState("initial")
63+
}
64+
}
65+
} finally {
66+
scope.cancel()
67+
}
68+
}
69+
}
70+
3871
CoroutineStarterType.entries.forEach { coroutineStarterType ->
3972
"$coroutineStarterType" - {
4073
"no initial state" {

0 commit comments

Comments
 (0)