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

OnEntry of the "terminating" state is not called when previous state is async #364

Open
Ed-Pavlov opened this issue Mar 30, 2020 · 5 comments

Comments

@Ed-Pavlov
Copy link

using System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Stateless;

namespace DeckTracker.Model.Tests
{
public class StateMachineTest
{
private const string InitialState = "InitialState";
private const string WorkState = "WorkState";
private const string StartWorkTrigger = "StartWorkTrigger";
private const string TerminatedState = "Terminated";
private const string TerminateTrigger = "Terminate";

[Test]
public void AsyncTest()
{
  var stateMachine = new StateMachine<string, string>(InitialState);

  stateMachine
    .Configure(InitialState)
    .Permit(StartWorkTrigger, WorkState);

  stateMachine
    .Configure(WorkState)
    .Permit(TerminateTrigger, TerminatedState)
    .OnEntryAsync(() => WorkAsync(() => stateMachine.IsInState(WorkState)));

  stateMachine
    .Configure(TerminatedState)
    .OnEntry(() => Console.WriteLine("Terminated")); // this method is not called at all
  
  stateMachine.FireAsync(StartWorkTrigger);
  
  Thread.Sleep(2597);
  stateMachine.Fire(TerminateTrigger);
}

private static async Task WorkAsync(Func<bool> inWorkingState)
{
  Console.WriteLine("Start working");
  while (inWorkingState())
  {
    Console.WriteLine("working...");
    await Task.Delay(1000);
  }
  Console.WriteLine("Stop working");
}

}
}

Test output:
Start working
working...
working...
working...

How do I wait till state machine handled "Terminated" state correctly?

@HenningNT
Copy link
Contributor

HenningNT commented Mar 31, 2020

I think this is a problem with using async await with void methods. The CLR will create its own state machine for every async await, and it will return code execution early if it can. The Console.WriteLine("Stop working"); is scheduled to be executed async, but the program has ended before it is done. This also applies to #363 .

What happens if you use the sync versions, and use await Task.Run( async ()=> stateMachine.Fire()); instead?

@HenningNT
Copy link
Contributor

If you wait a bit then you'll see the expected output:

class Program
{
    static void Main(string[] args)
    {
        var tester = new StateMachineTest();
        tester.Test();
        Console.ReadLine();
    }
}

Output:
Start working
working...
working...
working...
Terminated
Stop working

@Ed-Pavlov
Copy link
Author

Ed-Pavlov commented Mar 31, 2020

Of course OnEntry of Terminated state will be called in this case, but it's not as a state machine supposed to work, IMO. Fire should be either blocking till state machine completely changed the state or be async and provide a possibility to wait state changing.
BTW, if I change Fire(TerminateTrigger) to FireAsync(TerminateTrigger).Wait(); nothing changed.

@HenningNT
Copy link
Contributor

I agree, the non-Async version of Fire should not return early. And the Sync version should be Wait()-able...
I might do something about it, but I don't have that much time available :-(

@HenningNT
Copy link
Contributor

I had another look at this, and I think I was wrong.

When you call stateMachine.Fire(TerminateTrigger); the state machine is still busy executing the async fire, but it's doing it async, so it has returned the execution back to you. The OnEntryAsync is still running, and will never finish... Internally triggers are queued up, so they'll be handled sequentially.

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