appccelerate / statemachine

A .net library that lets you build state machines (hierarchical, async with fluent definition syntax and reporting capabilities).
Apache License 2.0
488 stars 128 forks source link

Exception in Execute action #36

Closed zorgoz closed 6 years ago

zorgoz commented 6 years ago

Hello,

I have to handle exceptions in transition somehow. Either go to another state or stay in the current state, but I can't deal with the actual behavior of the machine is such situation. See following code:

public enum States { S1, S2 }
public enum Actions { A1 }

async Task Main()
{
    var fsm = new AsyncPassiveStateMachine<States, Actions>();

    fsm.In(States.S1)
        .On(Actions.A1)
        .Goto(States.S2)
        .Execute(() => throw new Exception());

    fsm.In(States.S1)
        .ExecuteOnEntry(() => "Entry S1".Dump());

    fsm.In(States.S2)
        .ExecuteOnEntry(() => "Entry S2".Dump());

    fsm.TransitionExceptionThrown += (sender, args) => args.Dump();

    fsm.Initialize(States.S1);  

    await fsm.Fire(Actions.A1);

    await fsm.Start();

}

If there is no exception listener registered, the machine stops at exception. With one registered, it detects the exception but the transition is still performed, bringing the machine in an inconsistent state. Guards are of no use here.

Imagine following situation: a database operation is performed in the transition. But the connection fails meanwhile. I need to be noticed about it, but in no case should I step forward. How can I properly handle this in the machine?

Please advice!

zorgoz commented 6 years ago

On this page it is stated:

Extensions can change the behaviour of the state machine in...

  • Firing events on the state machine as a reaction to a call on the extension (e.g. perform transition to the error state on an exception)

I tried to folloow it in this attempt:

public enum States { S1, S2, E }
public enum Actions { A1, E }

async Task Main()
{
    var fsm = new AsyncPassiveStateMachine<States, Actions>();

    fsm.In(States.S1)
        .On(Actions.A1)
        .Goto(States.S2)
        .Execute(() => throw new Exception());

    fsm.In(States.S1)
        .ExecuteOnEntry(() => "Entry S1".Dump())
        .On(Actions.E).Goto(States.E)
        ;

    fsm.In(States.S2)
        .ExecuteOnEntry(() => "Entry S2".Dump())
        .On(Actions.E).Goto(States.E)
        ;

    fsm.In(States.E)
        .ExecuteOnEntry(() => "Entry E".Dump())
        ;

    fsm.TransitionExceptionThrown += (sender, args) => args.Dump();

    fsm.AddExtension(new Redirector<States, Actions>(fsm));

    fsm.Initialize(States.S1);  

    await fsm.Fire(Actions.A1);

    await fsm.Start();

}

public class Redirector<TState, TEvent> : AsyncExtensionBase<TState, TEvent>
        where TState : IComparable
        where TEvent : IComparable
{
    AsyncPassiveStateMachine<States, Actions> machine;

    public Redirector(AsyncPassiveStateMachine<States, Actions> machine)
    {
        this.machine = machine;
    }

    public override void HandlingTransitionException(IStateMachineInformation<TState, TEvent> stateMachine, ITransition<TState, TEvent> transition, ITransitionContext<TState, TEvent> context, ref Exception exception)
    {
        machine.FirePriority(Actions.E);
    }
}

Unfortunatelly the machine still enters S2 and the transition to E is performed ftom there, but only if it is defined. As I can't forcefully move the machine into a state, and I can't call Initialize either, the ITransition has no Abort method or something equivalent, thus no actual error state and error transition can be defined.

This is the simplest workaround I could figure out. But it performs a self-transition instread of an internal one, and it is really painfull to implement for a large machine:

public enum States { S1, S2 }
public enum Actions { A1, Back }

async Task Main()
{
    var fsm = new AsyncPassiveStateMachine<States, Actions>();
    bool withException = false;

    fsm.In(States.S1)
        .On(Actions.A1)
        .Goto(States.S2)
        .Execute(() => throw new Exception());

    fsm.In(States.S1)
        .ExecuteOnEntry(() => "Entry S1".Dump())
        ;

    fsm.In(States.S2)
        .ExecuteOnEntry(() =>
        {
            if (withException)
            {
                "Diversion...".Dump();
                fsm.FirePriority(Actions.Back);
                return;
            }

            "Entry S2".Dump();
        })
        .On(Actions.Back).Goto(States.S1)
        ;

    fsm.TransitionExceptionThrown += (sender, args) => withException = true;

    fsm.TransitionExceptionThrown += (sender, args) => args.Dump();

    fsm.Initialize(States.S1);  

    await fsm.Fire(Actions.A1);

    await fsm.Start();

}
ursenzler commented 6 years ago

It was a design decision that the state machine will end up in the target state of the transition even in the case of an exception. The reasoning is that the state machine was told to execute the transition and therefore cannot stay in the state it was.

Is it possible that you can handle the exception during the transition? So that in no case the exception is thrown at the state machine?

Otherwise, I see no easy way to achieve what you are looking for.

ursenzler commented 6 years ago

Or can you redesign the states like this:

(Waiting for something to write to the DB) --write--> (writing to the DB) --success--> (Done)
                                                                          --failed--> (Error state)
zorgoz commented 6 years ago

Thank you for your answer. Actually, as the machine is not independent of its environment, reacting to the exception with a specific transition would be the theoretical handling of the exception. The only workaround I could come up with is to split the transition in two:

S1*A1-(T)->S2A
S2A*null-()->S2B, 
S2A*error-()->S1

but as null transitions are also absent, it would make the machine far more complex.

zorgoz commented 6 years ago

Thank you for your suggestions, but to be able to produce maintainable code I have created my own state machine supporting both diversions on an exception and null transitions.