Closed pujunhui closed 1 month ago
Hi, the code sample has a deadlock.
1) the first problem is that there are two nested calls to runBlocking()
which is wrong (runBlocking and createStateMachineBlocking - uses runBlocking internally) use createStateMachine
instead.
2) the second is a deadlock of cancelAndJoin()
call. Replacing cancelAndJoin()
with cancel()
fixes the deadlock and the program completes as expected. The machine behaviour is correct.
At the moment I can't say the reason why cancelAndJoin()
deadlocks, I have to dig deeper to understand it.
Here is fixed sample:
package ru.nsk.samples
import kotlinx.coroutines.*
import ru.nsk.kstatemachine.event.DataEvent
import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.state.*
import ru.nsk.kstatemachine.statemachine.StateMachine
import ru.nsk.kstatemachine.statemachine.createStateMachine
import kotlin.time.Duration.Companion.seconds
private object LogoutEvent : Event
private object LoginEvent : Event
private data class ActiveEvent(val token: String) : DataEvent<String> {
override val data = token
}
private suspend fun test(scope: CoroutineScope): StateMachine {
return createStateMachine(
scope = scope,
name = "Test"
) machine@{
logger = StateMachine.Logger { println("TAG: ${it()}") }
val idleState = initialState("IdleState")
val authState = state("AuthState")
val workState = dataState<String>("WorkState")
idleState {
transition<LoginEvent> {
targetState = authState
}
}
authState {
var job: Job? = null
onEntry {
job = scope.launch {
val token = getToken() // getToken is a time-consuming method
this@machine.processEvent(ActiveEvent(token))
}
}
onExit {
job?.cancel()
job = null
}
transition<LogoutEvent> {
targetState = idleState
}
dataTransition<ActiveEvent, String> {
targetState = workState
}
}
workState {
onEntry {
println("TAG: workState entry")
}
}
}
}
private suspend fun getToken(): String {
delay(3.seconds)
return "token123"
}
fun main(): Unit = runBlocking {
val machine = test(this)
machine.processEvent(LoginEvent)
}
and the output
TAG: StateMachineImpl(Test) started
TAG: StartEventImpl triggers DefaultTransition(start transition) from StateMachineImpl(Test) to [StateMachineImpl(Test)]
TAG: Parent StateMachineImpl(Test) entering child DefaultState(IdleState)
TAG: LoginEvent triggers DefaultTransition from DefaultState(IdleState) to [DefaultState(AuthState)]
TAG: Exiting DefaultState(IdleState)
TAG: Parent StateMachineImpl(Test) entering child DefaultState(AuthState)
TAG: ActiveEvent triggers DefaultTransition from DefaultState(AuthState) to [DefaultDataState(WorkState)]
TAG: Exiting DefaultState(AuthState)
TAG: Parent StateMachineImpl(Test) entering child DefaultDataState(WorkState)
TAG: workState entry
Ahh the deadlock reason is simple,
when we call cancellAndJoin()
from onExit()
callback we block the machine execution. And processEvent(ActiveEvent(token))
call cannot ever complete. So they are waiting for each other.
You can use either cancel()
as in sample above or if you need to do something after the job is joined use cancelAndJoin()
but asynchronously:
onExit {
scope.launch {
job?.cancelAndJoin()
job = null
println("job has been canceled and joined")
}
}
I have the following code:
execute result
when i get token at authState, transition to workState, but running at authState#onExit,will cancel the job,than cause the processEvent cancel,As a result, the state machine did not enter the workState?
Is this a design flaw or am I using it incorrectly?