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

Internal transitions in nested state seems to trigger multiple actions #63

Open
hrosa opened this issue Oct 18, 2016 · 5 comments
Open

Comments

@hrosa
Copy link

hrosa commented Oct 18, 2016

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

hrosa commented Nov 23, 2016

@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?

@hekailiang
Copy link
Owner

go head, pass all the unit test

@hrosa
Copy link
Author

hrosa commented Nov 24, 2016

@hekailiang any hints on where I should start looking?

@hrosa
Copy link
Author

hrosa commented Nov 25, 2016

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

hrosa commented Nov 25, 2016

Created PR

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

No branches or pull requests

2 participants