Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sarahborgi opened this issue May 13, 2024 · 11 comments

Comments

@sarahborgi
Copy link

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
Copy link
Member

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

@sarahborgi
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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
Copy link
Author

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
Copy link
Member

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
Copy link
Contributor

@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
Copy link
Author

@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 actionMapper to 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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants