freeletics / FlowRedux

Kotlin Multiplatform Statemachine library with nice DSL based on Flow from Kotlin Coroutine's.
https://freeletics.github.io/FlowRedux/
Apache License 2.0
700 stars 27 forks source link

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

Open sarahborgi opened 4 months ago

sarahborgi commented 4 months ago

We've integrated the FlowRedux library to manage our app's state, and things have been running smoothly. However, occasionally and unexpectedly, the app crashes and we can see in the traces the "Cannot dispatch action because state Flow of this FlowReduxStateMachine is not collected yet" error. This error affects various parts of the app. Oddly, it only occurs in the release version, while the debug version remains unaffected.

gabrielittner commented 4 months ago

Could you share how you are collecting the state machine, specifically based on what you're starting/stopping?

sarahborgi commented 4 months ago

Here's how I'm starting the state machine:

class StateMachineProcessor {

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

    fun start() {
    stateMachine = FirstStateMachine()
        transactionFlowScope.launch {
            stateMachine.stateFlow.collect { state ->
              trace.d { "State Machine State: $state" }
                status = state
                if (state == State.Idle) {
                    this.cancel()
                }
            }
        }
    }

The state machine is stopped when it reaches the Idle state

gabrielittner commented 4 months ago

Do you know whether this happens before the start machine starts running or after it's stopped? In either case the safest would be to guard actions being sent based on status not being Idle.

I was thinking about having an option to relax these checks. What would you expect to happen with an action that is dispatched while the state machine is not running? It could either be dropped or kept internally until the state machine starts. I'm leaning towards the latter since dropping can be achieved with a guard and when the state machine starts it will only handle those actions if it is on the right state.

sarahborgi commented 4 months ago

This is happening when the state machine is running and haven't reached the idle state yet. To provide more context, 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())
    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 = {
                       it
                    }
                )
            }
            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 {
           inState<State.Processing> {
                condition({ state ->
                    state.subState == State.Processing
                })
            {
                on { _: Event.ToError, state ->
                    state.mutate { copy(subState = State.Error) }
                }
               on { _: Event.ToSucess, state ->
                    state.mutate { copy(subState = State.Sucess) }
                }
            }

        }
    }
}

the crash happens when we signal events such as Event.ToSucess or Event.ToError to the FirstStateMachine, these crashes are random and only occurs on the release version. Everything works smoothly on the debug version.

sarahborgi commented 3 months ago

I wanted to highlight that this issue is currently blocking our release, and finding a solution as soon as possible is critical for us. The crashes occur when we signal events such as Event.ToSuccess or Event.ToError to the FirstStateMachine, and these crashes appear to be random, only happening in the release version while the debug version remains unaffected.

Is there any additional information or context you could provide that might help us better understand the root cause of this problem? Specifically, any insights on why this might only be occurring in the release version would be greatly appreciated.

We are eager to resolve this issue promptly and would greatly value any further guidance or suggestions you can offer.

Thank you in advance for your assistance.

gabrielittner commented 3 months ago

Hi, sorry for the late reply I was out for a bit and then very busy.

Is the exception that's causing the crash thrown by FirstStateMachine or by SecondStateMachine? Since you mentioned that it happens when the state machine is not idle I'm assuming it's SecondStateMachine?

hoc081098 commented 3 months ago

@sarahborgi I think you should provide the stack trace of crash. Logging dispatched actions and changed states of these state machines are helpful to investigate more easily 🙏

sarahborgi commented 3 months ago

@gabrielittner @hoc081098 Thank you for your continued assistance. The crashes occur when events like Event.ToSuccess or Event.ToError are dispatched to the FirstStateMachine, and these events are mapped by the actionMapperto the SecondStateMachine. While the provided example is simplified, our actual implementation includes many more events and states, and we are confident there is no overlapping of actions. We suspect obfuscation in the release build might be contributing to this issue, as the crashes are random and only occur in the release version, with the debug version unaffected. Here is the exception stack trace that happens randomly:

2024-02-07 13:24:49 E/ [java.lang.ThreadGroup:uncaughtException 1073] Uncaught exception - Cannot dispatch action Event.Finalize because state Flow of this FlowReduxStateMachine is not collected yet. Start collecting the state Flow before dispatching any action.
java.lang.IllegalStateException: Cannot dispatch action Event.Finalize because state Flow of this FlowReduxStateMachine is not collected yet. Start collecting the state Flow before dispatching any action.
    at b1.j.a(Unknown Source:51)
    at c1.p.e(Unknown Source:24)
    at c1.c.e(Unknown Source:121)
    at v5.p.j(Unknown Source:128)
    at i9.i.r(Unknown Source:11)
    at o8.a.p(Unknown Source:7)
    at f9.j0.run(Unknown Source:112)
    at l9.a.run(Unknown Source:91)
    Suppressed: k9.f: [s1{Cancelling}@381cdc1, Dispatchers.Default]
gabrielittner commented 3 months ago

Maybe trying to exclude all of com.freeletics.flowredux.** and your state machine classes from optimizations and obfuscations is worth a try? Not as a permanent solution but it would at least tell us it's really related to that and we can use that as a starting point to look into where exactly the issue is.

In the mean time I'll check again if I can see anything around the stopping of sub state machines that could cause the issue. Also if we can make the error message more helpful in finding out what's going on.

Question about your example (just not sure if it's from the simplification or not): The exception has Event.Finalize is that dispatched after ToError/ToSucess event that finishes the sub state machine? The reason why I'm asking is, in the sample ToError/ToSucess move the state machine to a state that causes the sub state machine to be stopped. If Event.Finalize is an event that can be sent to FirstStateMachine after one those other 2 then it might be that there is an race condition in shutting down the sub state machine. So based on your actual code could the following order of events happen?

  1. ToError/ToSucess is dispatched to FirstStateMachine
  2. ToError/ToSucess is forwarded to SecondStateMachine
  3. Moves to Error/Success state
  4. Because of the state change SecondStateMachine is not being collected anymore
  5. Finalize is dispatched to FirstStateMachine
  6. There is an issue inside FlowRedux and it still tries to sent Finalize to SecondStateMachine even though that isn't collected anymore which then causes the exception you're seeing
gabrielittner commented 3 months ago

If it's not related to optimization/obfuscation and something like I mentioned in my last comment then 1.2.2 might fix it.

gabrielittner commented 3 months ago

@sarahborgi Did you have a chance to try out 1.2.2?