spring-statemachine
spring-statemachine copied to clipboard
Transitions from pseudo states do not trigger @OnTransition
Hi, I have a state machine defined through UML. Please see my sample state machine diagram attached. The flow goes like this:
After the start, the sm goes directly to the state S1. Then based on the CHOICE it goes either to state S2 or directly to state S3. In the state S2 it waits for a signal and if the signal is received in goes to S3. After another signal the sm moves from S3 to the final state.
I have implemented two beans TransitionHandler and StateHandler to just log events with methods annotated with @OnTransition, @OnTransitionStart, @OnTransitionEnd, @OnStateEntry, @OnStateExit. In the StateHandler I am logging the source and target states from the context. In the TransitionHandler I am logging the source and target from the transition object, I have observed some weird behavior:
1.) The transition T4 does not trigger the @OnTransition method 2.) Assume the flow over the transition T4 (i.e. skipping the S2). The log output is following until the S3:
[main] INFO TransitionHandler - @OnTransitionStart: : from 'null' to 'S1' [main] INFO TransitionHandler - @OnTransition: : from 'null' to 'S1' [main] INFO StateHandler - @OnStateEntry: null -> [S1] [main] INFO TransitionHandler - @OnTransitionStart: : from 'S1' to 'Choice' [main] INFO TransitionHandler - @OnTransition: : from 'S1' to 'Choice' [main] INFO StateHandler - @OnStateExit: [S1] -> null [main] INFO StateHandler - @OnStateEntry: [S1] -> [S3] [main] INFO TransitionHandler - @OnTransitionEnd: : from 'S1' to 'Choice' [main] INFO TransitionHandler - @OnTransitionEnd: : from 'null' to 'S1'
As you see, following problems occured:
- @OnTransitionEnd for the transitions T1 and T2 was triggered as the very last one
- @OnTransitionStart and @OnTransition was completely ignored for T4
- @OnStateEntry has empty source state in the context (shouldn't be Start?)
- @OnStateExit has empty target for exiting the S1, although in the transition T2 the source and target is correct.
To summarize, what are the rules for the order of the events and when and why are the source/targets empty?
Thx for writing these down. I think we need to overhaul how these hooks are called(it's also same issue with listeners afaik). It's that choice
pseudostate messing things up as it's not a normal state and thus should not be part of these notifications. Pseudostates(apart from end/final state which is a special state) are never entered/exited as they are just on-transit always leading to some real state. Thought maybe we should try to relax this concept a little and try to notify every step as I also kinda see it beneficial to know how the whole execution flow goes.
Maybe we should add new methods which would get called when these pseudostate are in play as calling @OnStateEntry
where target is a pseudostate is just wrong as its never entered as it's not a real state.
Thanks for the reply. I've been digging inside the spring state machine and managed to override the UmlStateMachineModelFactory, where I added a custom Action implementation to every transition actions list and every choice actions list. In this action I would like to decide based on the Source/Target what to execute. And here comes the problem.
Even If I hook up on the T3 action from the diagram, inside the transition action the context will have S1 as the source and Choice as the target, although in that moment the state machine already knows that the target is going to be S2.
Could not the implementation in the ChoicePseudoState in the entry method change the transition in the context to the new one? The thing is, in any transition action coming from a pseudo state it is impossible to actually get the information to which state it is changing to.
If there is any workaround, I would be thankful.
Unfortunately there's no workaround for behaviour for this behaviour as it's embedded in so deep in a core. I started to craft new tests around notification features(as it's really same for annotation callbacks as for a listener callbacks). I think this ticket will result a new epic ticket to overhaul behaviour around notifications as some issues are just about docs, some are most likely breaking features as what should be in a StateContext
, etc. StateContext
really is a bit of a beast as its role is different depending on a Stage
in it as it is used for various stages like state/transition/machine events.
There's probably many things I'd do differently if I could re-write everything but lets wait when I get a change to create epic and start adding additional stories under it.
These transitions around pseudostates are really a beasts as they are just lines in a statechart but in a realworld multiple paths are taken in a same transition step simply because pseudostates are just transient non-normal states.
One problem is that i.e. when target is a choice pseudostate, we don't know where machine will go until that choice state is negotiated and it tells its target state. Normally you would not care but indeed it would be nice to get notification for that transition leading out from a choice state.
We see similar behavior where our StateMachineListenerAdapter
method is not called for ASSEMBLED
state like configured in the snippet below. Also here it's a choice, and as it seems, one of the pseudostates you're referring to, right?
.and()
.withExternal().event(OrderEvent.ASSEMBLED)
.source(State.WAITING_FOR_VENDOR).target(State.ASSEMBLED)
.and()
.withChoice().source(State.ASSEMBLED)
.first(State.RECONCILIATION_STARTED, actionProvider.isReconciliationNeeded(),
actionProvider.reconcileOrder(), actionProvider.generalErrorHandler())
.last(State.WAITING_FOR_PICKUP)
Since that re-write sounds like it's a big bunch of work, what would be the suggestion to handle such a case (getting notified about ASSEMBLED
(choice)) with the current state machine version?
@jvalkeal Do you happen to know if such transition was ever implemented? We also need to know in case a choice pseudo state entry action has failed to promptly catch e do some cleanup. Since we are tied with kryo 4, our exceptions need to be cleaned from context, other it fails to be serialized in the subsequent call while reading