dotnet-state-machine / stateless

A simple library for creating state machines in C# code
Other
5.41k stars 746 forks source link

Execute guards in parallel? #578

Open dave-signify opened 1 month ago

dave-signify commented 1 month ago

Hello!

First off, thanks for the lib. Really enjoying using it, and great work 👍 😄

I have a question related to guard logic execution.

Does stateless support executing guards in parallel?

For example, in the below snippet I create a new statemachine and I configure a transition. The transition will evaluate 3 independent guard conditions. Each Guard takes 1 second to complete. Therefore the overall guard execution time will accumulate to 3 seconds.

var stateMachine = new StateMachine<MyState, MyTrigger>(MyState.Off);

stateMachine.Configure(MyState.Off)
            .PermitIf(MyTrigger.FlickSwitch, MyState.On, 
                guards: new []
                { 
                    new Tuple<Func<bool>, string>(() =>
                    {
                        Thread.Sleep(millisecondsTimeout:1000);
                        return true;
                    }, "First Guard"),
                    new Tuple<Func<bool>, string>(() =>
                    {
                        Thread.Sleep(millisecondsTimeout:1000);
                        return true;
                    }, "Second Guard"),
                    new Tuple<Func<bool>, string>(() =>
                    {
                        Thread.Sleep(millisecondsTimeout:1000);
                        return true;
                    }, "Third Guard")
                });

For performance reasons I would like to execute all the guards in parallel to reduce overall execution time. The above example is arbitrary, but in an ideal scenario I would like the overall guard execution time to be equal to the longest running guard. In the case above the guard execution logic would execute in parallel taking roughly 1 second (give or take a few milliseconds), rather than 3 seconds.

Does stateless support this funtionality? I haven't found an API in the framework that supports this unless I'm missing something? To get around this I ended up creating a wrapper class that holds n number of guard functions. The class exposes a single API called ExecuteinParrallel that I then pass by reference to the PermitIf function in stateless. This allows me to achieve the performance optimisation I'm looking for.

It would be neat if this functionality was provided out of the box by stateless. If this feature is not yet supported, I'd be happy to open a PR to add this functionality?

Thanks again! D

mclift commented 1 month ago

As an interim solution, and if you don't mind losing the guard descriptions, could you wrap the calls inside a Task.WhenAll? Something like:

            stateMachine.Configure(MyState.Off)
                .PermitIf(MyTrigger.FlickSwitch, MyState.On, () => (Task.WhenAll(
                    Task.Run(() =>
                    {
                        Thread.Sleep(millisecondsTimeout: 1000);
                        return true;
                    }),
                    Task.Run(() =>
                    {
                        Thread.Sleep(millisecondsTimeout: 1000);
                        return true;
                    }),
                    Task.Run(() =>
                    {
                        Thread.Sleep(millisecondsTimeout: 1000);
                        return true;
                    })
                )).Result.All(x => x));
dave-signify commented 1 month ago

Thanks for the prompt reply @mclift 👍 and I hope you had a nice weekend! 😄

Yes, this interim solution is more or less what I have done in my implementation. Good to know I was on the right track. My solution however does retain the guard descriptors as it's important that I know which guard during execution returned false.

I was thinking of opening a PR to move this parallel guard functionality into the stateless library as a first class feature for you to review? Would this be okay?

Thanks again, Dave

mclift commented 3 weeks ago

Thanks for your patience, @dave-signify.

I've been mulling this over as the idea of having Stateless spin up multiple threads for parallel execution of guard functions isn't sitting well with me. I think the crux of it is that if the caller needs finer control over how the guard functions run, we should probably just delegate the execution strategy to the caller. So I wonder if there should be a way to configure a state transition with another delegate that provides that custom guard execution. Any thoughts?

Mike