akkadotnet / akka.net

Canonical actor model implementation for .NET with local + distributed actors in C# and F#.
http://getakka.net
Other
4.7k stars 1.04k forks source link

Issue with PersistentFSM StateData updating #3285

Open gztamas opened 6 years ago

gztamas commented 6 years ago

Hi, I've ran into what seems like a bug in the persistent FSM. I was able to reproduce it in a small application I put here: https://github.com/gztamas/AkkaNetPersistentFsmGoToIssue

Using Akka 1.3.2 I encountered a behavior that I find curious. I created an actor based on the PersistentFSM. It has a small state object, with an IActorRef and a string property. When the FSM receives a command to update the string, it should either stay in the default state or move to a different one, depending on whether the actor reference is nobody.

I created some tests to verify the correct behavior and I kept getting a red test. After a lot of trial and error I narrowed it down. It looks like if I do not transition state and use Stay().Applying(...) or even GoTo(myCurrentState), the state of the UnderlyingObject in the TestActorRef changes as expected, but when I use GoTo() specifying another state, even though I can see the ApplyEvent running as expected, when investingating the UnderlyingObject in the test, it's as if the state data did not change.

So the following statement: AwaitAssert(() => subject.UnderlyingActor.StateData.Stuff.Should().Be(newStuff)); results in a green test if I use Stay() and do not change state, but breaks if I use GoTo() to transition to a new state (but is green if I "go to" the same state).

You can reproduce this by (un)commenting the proper lines in SampleActor.cs

Is this an issue with the persistent FSM or am I messing up (or not understanding) something?

gztamas commented 6 years ago

Upon some further investigation I found an additional point. In the sample I created two classes for state, but only added one state handler in the constructor (not out of sloppyness, I was doing it TDD-style and I didn't get to the second handler yet). Today just on a hunch I added a handler for the second state. I wanted to see what happens "after" the issue. So I introduced this piece of code:

When(WaitingState.Instance, (evt, state) =>
{
    if (evt.FsmEvent is Commands.SetRef)
    {
        var command = (Commands.SetRef) evt.FsmEvent;
        var @event = new DomainEvents.RefSet(command.Ref);

        return GoTo(DefaultState.Instance)
            .Applying(@event)
            .AndThen(x => Context.System.EventStream.Publish(@event));
    }

    return Stay();
});

So nothing special. Without modifying any of the tests, to my surprise they all passed. I stashed the change to verify that the test breaks without this handler that is not used by any of them. It does break.

I found this interesting enough and replaced the new piece of code with this:

When(WaitingState.Instance, (evt, state) => Stay());

The test is green again.

Now this answers the question I asked myself, how can anything possibly work using the PersistentFSM if it cannot do a state transition and apply domain events at the same time (which is the whole point of the thing). Turns out it does work if all the handlers are specified (?).

So I started checking the docs, and found these lines in the (not persistent) FSM:

Within the body of the actor a DSL is used for declaring the state machine:

  • StartWith defines the initial state and initial data
  • then there is one When(, () => {}) declaration per state to be handled [emphasis by me]
  • finally starting it up using initialize, which performs the transition into the initial state and sets up timers (if required).

Ok, so there is such a prerequisite. I accept it, and honestly am glad that I was at fault, for I can now continue working. But the way this broke my tests, I don't find it helpful or meaningful. Maybe it can be put on some low prio wishlist to introduce some sort of validation that breaks when the FSM is being started?