Open sarahborgi opened 4 months ago
Could you share how you are collecting the state machine, specifically based on what you're starting/stopping?
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
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.
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.
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.
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
?
@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 🙏
@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]
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?
ToError
/ToSucess
is dispatched to FirstStateMachine
ToError
/ToSucess
is forwarded to SecondStateMachine
Error
/Success
stateSecondStateMachine
is not being collected anymoreFinalize
is dispatched to FirstStateMachine
Finalize
to SecondStateMachine
even though that isn't collected anymore which then causes the exception you're seeingIf it's not related to optimization/obfuscation and something like I mentioned in my last comment then 1.2.2 might fix it.
@sarahborgi Did you have a chance to try out 1.2.2?
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.