squirrel icon indicating copy to clipboard operation
squirrel copied to clipboard

Internal transitions in nested state seems to trigger multiple actions

Open hrosa opened this issue 8 years ago • 5 comments

Hi,

I've started to use Squirrel recently to build an FSM for some media operations as defined here. So far, the framework seems to have a lot of nice features and is pretty flexible. Great job!

I think I came across a bug though. To implement the PlayCollect feature, I need Play and Collect states to execute in parallel. Like this:

squirrel-internal-transition-bug

This is how the FSM is setup:

this.builder.defineFinishEvent(PlayRecordEvent.EVALUATE);

        this.builder.onEntry(PlayRecordState.READY).callMethod("enterReady");
        this.builder.transition().from(PlayRecordState.READY).to(PlayRecordState.ACTIVE).on(PlayRecordEvent.START);
        this.builder.onExit(PlayRecordState.READY).callMethod("exitReady");

        this.builder.onEntry(PlayRecordState.ACTIVE).callMethod("enterActive");
        this.builder.transition().from(PlayRecordState.ACTIVE).to(PlayRecordState.READY).on(PlayRecordEvent.EVALUATE);
        this.builder.transition().from(PlayRecordState.ACTIVE).toFinal(PlayRecordState.TIMED_OUT).on(PlayRecordEvent.TIMEOUT);
        this.builder.onExit(PlayRecordState.ACTIVE).callMethod("exitActive");
        this.builder.defineParallelStatesOn(PlayRecordState.ACTIVE, PlayRecordState.COLLECT, PlayRecordState.PROMPT);

        this.builder.defineSequentialStatesOn(PlayRecordState.PROMPT, PlayRecordState._PROMPTING, PlayRecordState._PROMPTED);
        this.builder.onEntry(PlayRecordState._PROMPTING).callMethod("enterPrompting");
        this.builder.internalTransition().within(PlayRecordState._PROMPTING).on(PlayRecordEvent.NEXT_TRACK).callMethod("onPrompting");
        this.builder.transition().from(PlayRecordState._PROMPTING).toFinal(PlayRecordState._PROMPTED).on(PlayRecordEvent.PROMPT_END);
        this.builder.onExit(PlayRecordState._PROMPTING).callMethod("exitPrompting");

        this.builder.onEntry(PlayRecordState._PROMPTED).callMethod("enterPrompted");
        this.builder.onExit(PlayRecordState._PROMPTED).callMethod("exitPrompted");

        this.builder.defineSequentialStatesOn(PlayRecordState.COLLECT, PlayRecordState._COLLECTING, PlayRecordState._COLLECTED);
        this.builder.onEntry(PlayRecordState._COLLECTING).callMethod("enterCollecting");
        this.builder.internalTransition().within(PlayRecordState._COLLECTING).on(PlayRecordEvent.DTMF_TONE).callMethod("onCollecting");
        this.builder.transition().from(PlayRecordState._COLLECTING).toFinal(PlayRecordState._COLLECTED).on(PlayRecordEvent.END_COLLECT);
        this.builder.onExit(PlayRecordState._COLLECTING).callMethod("exitCollecting");

        this.builder.onEntry(PlayRecordState._COLLECTED).callMethod("enterCollected");
        this.builder.onExit(PlayRecordState._COLLECTED).callMethod("exitCollected");

        this.builder.onEntry(PlayRecordState.TIMED_OUT).callMethod("enterTimedOut");

The problem I came across is related to internal state transition in nested states. Like PROMPTING -> Prompting on NEXT_TRACK. It seems to trigger more actions than intended.

Here is a simple test case:

    @Test
    public void testPromptCollect() {
        // given
        final MgcpEventSubject mgcpEventSubject = mock(MgcpEventSubject.class);
        final Recorder recorder = mock(Recorder.class);
        final RecorderListener recorderListener = mock(RecorderListener.class);
        final DtmfDetector detector = mock(DtmfDetector.class);
        final DtmfDetectorListener detectorListener = mock(DtmfDetectorListener.class);
        final Player player = mock(Player.class);
        final PlayerListener playerListener = mock(PlayerListener.class);
        final PlayRecordContext context = new PlayRecordContext();
        final PlayRecordFsm fsm = PlayRecordFsmBuilder.INSTANCE.build(mgcpEventSubject, recorder, recorderListener, detector, detectorListener, player, playerListener, context);

        // when
        fsm.start();
        fsm.fire(PlayRecordEvent.START, context);
        fsm.fire(PlayRecordEvent.NEXT_TRACK, context);
        fsm.fire(PlayRecordEvent.NEXT_TRACK, context);
        fsm.fire(PlayRecordEvent.NEXT_TRACK, context);
        fsm.fire(PlayRecordEvent.NEXT_TRACK, context);
        fsm.fire(PlayRecordEvent.END_COLLECT, context);
        fsm.fire(PlayRecordEvent.PROMPT_END, context);
    }

You can find the complete log here squirrel-internal-transition-bug.txt.

It seems more actions are triggered the greater the number of local transitions we have.

Can you please look into this and confirm it's a bug?

Thanks in advance!

hrosa avatar Oct 18 '16 07:10 hrosa

@hekailiang I'm willing to contribute with a patch for this issue. Since I'm unfamiliar with code at the moment, could you provide any hints for me to get started?

hrosa avatar Nov 23 '16 02:11 hrosa

go head, pass all the unit test

hekailiang avatar Nov 24 '16 11:11 hekailiang

@hekailiang any hints on where I should start looking?

hrosa avatar Nov 24 '16 19:11 hrosa

@hekailiang Was able to find the problem.

This line here https://github.com/hekailiang/squirrel/blob/master/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateImpl.java#L390 can create duplicate transitions in the sub-state.

In the following example from debugger we clearly see two duplicate COLLECTING sub-states:

screen shot 2016-11-25 at 02 10 05

I'm planning to avoiding duplication of entries here: https://github.com/hekailiang/squirrel/blob/master/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateMachineDataImpl.java#L174

&& !parallelStatesStore.containsEntry(parentStateId, subStateId)

Does this seem correct to you? Test suite is passing and this solves my issue.

hrosa avatar Nov 25 '16 16:11 hrosa

Created PR

hrosa avatar Nov 25 '16 17:11 hrosa