freeletics / FlowRedux

Kotlin Multiplatform Statemachine library with nice DSL based on Flow from Kotlin Coroutine's.

Home Page:https://freeletics.github.io/FlowRedux/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cannot dispatch action because state Flow of this FlowReduxStateMachine is not collected yet

sarahborgi opened this issue · comments

I am encountering an issue with the state machine where it occasionally crashes when two dispatches are closely timed(26 ms). Unfortunately, I don't have control over when events are dispatched as it depends on another application that I have no control over. Despite traces indicating that the state is being collected correctly, the state machine crashes intermittently(9 out of 10 times). The crash occurs even when the state is being collected properly. Additionally, I have tried different solutions to synchronize the dispatching of events, but none have been successful.

Here's a simplified version of how our state machine looks like:

internal sealed class State {

    object Idle : State()
    object Start: State()
    data class Processing (
        var subState: State(),
        val parentStateMachine: FlowReduxStateMachine<State, Event>? = null
    )
    object Error: State()
    object Success: State()
}

sealed class Event {
    object Processing: Event()
    object ToError: Event()
    object ToSucess : Event()
    object GoBack: Event()
}

@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
internal class FirstStateMachine(
) : FlowReduxStateMachine<State, Event>(initialState = State.Start)

val currentFlow: FlowReduxStateMachine<State, Event> = this
var nextFlowBuilder: IPostProcessingFlowBuilder? = null
{

    init {
        spec {
            inState<State.Start> {
                on {
                    _, Event.Processing->
                    state.override { State.Processing}
                }
            }
            inState<State.Processing> {
                onEnterStartStateMachine(
                    stateMachineFactory = {
                        nextFlowBuilder as FlowReduxStateMachine<State, Event>
                    },
                    stateMapper = { state, subState ->
                        state.override { subState }
                    },
                    actionMapper = {
                       null
                    }
                )
            }
            inState<State> {
                on { _: Event.GoBack, state ->
                    state.override { State.Idle }
                }
            }
        }
    }
     val stateFlow: SharedFlow<State> = state.shareIn(
        scope = transactionFlowScope,
        started = SharingStarted.Eagerly,
        replay = 1
    )
}

@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
internal class SecondStateMachine(
) : IPostProcessingFlowBuilder, FlowReduxStateMachine<State, Event>(initialState)

val currentFlow: FlowReduxStateMachine<State, Event> = this
{

    init {
        spec {
            inStateWithCondition({ state: State.Processing->
                state.subState == State.Processing
            })
            {
                on { _: Event.ToError, state ->
                    state.mutate { copy(subState = State.Error) }
                }
               on { _: Event.ToSucess, state ->
                    state.mutate { copy(subState = State.Sucess) }
                }
            }

        }
    }
}

Starting the state machine :

        transactionFlowScope.launch {
            stateMachine.stateFlow.collect { state ->
                println(state.toString())
                status = state
               
                if (state == State.Idle) {
                    this.cancel()
                }
            }
        }

The idea here is before entering the Processing state a certain message from another application is responsible for choosing what is the next state machine we're going to and therefore triggering the transition to processing state, and then almost immediately another message is triggering the ToError event which is causing a crash

Hello,
I dont understand how you start FirstStateMachine, in otherwords I don't see any FirstStateMachine.state.collect {...} calls in the snippets above.

How does FirstStateMachine.stateFlow connect to FirstStateMachine.state?

As an additional question, which of the 2 state machines receives the dispatch when the crash happens?

@sockeqwe stateFlow is defined as a property for FirstStateMachine as follows :

val stateFlow: SharedFlow<State> = state.shareIn(
        scope = transactionFlowScope,
        started = SharingStarted.Eagerly,
        replay = 1
    )

Here's how I'm starting the FirstStateMachine :

class StateMachineProcessor {

    var status: State = State.Idle
    private lateinit var stateMachine: FirstStateMachine

 
    fun start() {
	stateMachine = FirstStateMachine()
        transactionFlowScope.launch {
            stateMachine.stateFlow.collect { state ->
                println(state.toString())
                status = state
                if (state == State.Idle) {
                    this.cancel()
                }
            }
        }
    }

@gabrielittner the crash happens when we signal the Event.ToSucess or Event.ToError to the SecondStateMachine

I made a diagram to explain where exactly the crash is happening:
FlowRedux
I'd like to also mention that our solution operated without issues previously, the crash is now seen because Event.Processing and Event.ToError are closely timed.

Are you dispatching actions directly to SecondStateMachine? I'm assuming yes since you have actionMapper = { null } in FirstStateMachine. The best way to solve the issue would be to not dispatch to sub state machines directly and instead dispatch everything to FirstStateMachine. If needed you could have a special event that wraps a regular event and unwrap it in actionMapper (to only forward those special events and to not handle them in FirstStateMachine). The implementation of onEnterStartMachine has a lock that waits for the sub state machine to be started before starting to forward actions.

This works, thanks

Great to hear, I'm going to close this then