-
Notifications
You must be signed in to change notification settings - Fork 129
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
Stateless operation? #19
Comments
Hmm, given that the current implementation has associated a queue with an instance, it doesn't look like it's feasible to use the model in this way. |
Hi Jason There is more state in the state machine than just the current state. There is also the knowledge about what child state was last active in a super state (history). And there is also the "state" of the queue of events to process. Therefore, I do not see a way to make your wish reality. Extracting the definition from the state machine would probably help. As you stated, the goal is to make instantiating a state machine very fast. Currently it is not because defining a state machine is quite expensive due to validation checks that the definition makes sense. If you think, that extracting the definition would help you, then I'll find some time to implement this. Or you may make a PR :-) Happy coding |
Yes, I noticed and mentioned those two aspects of the current system. Please don't take this as criticism of the code you've done here. It's very nicely done, and has a number of distinct advantages over things like the 'stateless' library which only supports actions on entry/exit of states, and doesn't even acknowledge that transactions can be required to perform actions :) |
Note that there is already a split between the core machine (in the namespace Machine) and the part taking care of the queues. But you have probably already seen that :-) Currently, the real state, the definition and the history data is tightly coupled together in the State class. A fact I do not like anyway (nowadays). So I agree with you, but I probably have to spend some time with the code to see how this could be changed. It's a while since I made changes in the core of the machine. Thanks for rising this issue. It's the only way to get real improvement into this project. I'll be on vacation for the next 2 weeks, so I'll be slow responding... |
Just wanted to let you know that I started to look into this. Unfortunately, the code resists this change (design not really a match for this feature). I hope I can soon present a working prototype. |
Just to let you know, I'm working on this, but it results in a really big change for the state machine. So don't count on having a solution any time soon :-( |
No problems. I tried contacting you on Gitter a few months ago, but had no luck. Execution is almost instantaneous, and I designed it to handle hundreds of thousands of simultaneous users being tracked through smoking cessation processes. It's hopefully to be implemented in China where they have 300 million smokers, so performance is quite important :) Anyway, feel free to use, ignore, or rework anything if it's of interest, otherwise, feel free to ignore it 👍 |
using System;
using System.Collections.Generic;
using System.Linq;
using ServiceStack.Text;
namespace FastFSM
{
public abstract class FSM<TStateName, TEvent, TEventArgs> where TEvent : IComparable where TStateName : IComparable
{
public FSM()
{
}
public FSM(string name, string description, List<State> states)
{
Name = name;
Description = description;
States = states;
}
// [{"Type":"State","Groups":[],"Name":"START","Tag":"","Transitions":[{"Type":"Transition","Actions":"Console.WriteLine(\"Blah!\");\nvar X = 99;\nvar Y = X + 99;\n","Description":"SMS set quit day","DestinationState":"QUIT_DAY","Event":"","Guard":"return (User.Event.EventName==\"SMS\" && User.Event.Data.Arguments==\"QD\")","metadata":{"points":[70.60688424157995,59.280055660151504,136.42305214076973,101.95446142051085,194.94207506334004,152.52045725669217,245.60136903821768,205]}},{"Type":"Transition","Actions":"","Description":"SMS stop","DestinationState":"QUIT","Event":"","metadata":{"points":[10.385456661284017,64.6530904108268,-29.064712586623884,101.64215948222278,-73.15277510043195,130.48388328068634,-126,157.15241466719647]}},{"Type":"Transition","Actions":"","Description":"START to ZZZZ","DestinationState":"ZZZZ","Event":"","Guard":"return User.Event.EventName==\"XX\" && User.Event.Source==\"TIMER\"","metadata":{"points":[76.77736641333044,39.197200719891086,275.83608957757286,43.368258809857835,473.62409822054997,60.89275628991601,670,89.9155844032866]}}]},{"Type":"State","Description":"User set quit day","Groups":[],"Name":"QUIT_DAY","Tag":"","Transitions":[{"Type":"Transition","Actions":"","Description":"Quit => X","DestinationState":"X","Event":"","Guard":"return true;","metadata":{"points":[243,338.66214334184997,187.9233412084239,381.74100804613795,124.25667454175722,422.25615956128945,52,458.9318577555296]}},{"Type":"Transition","Actions":"","Description":"SMS temp stop","DestinationState":"TEMP_STOP","Event":"","Guard":"return User.Data(\"ST\").Number() > 5;","metadata":{"points":[323.6631603776243,355,329.61252100241876,433.79028971571535,327.37442576432346,512.1236230490487,316.99553994411133,590]}}],"metadata":{"loc":"318 280"}},{"Type":"State","Description":"No longer wants to quit smoking.","Groups":[],"Name":"QUIT","Tag":"","Transitions":[],"metadata":{"loc":"-201 195"}},{"Type":"State","Description":"Xxxxx","Groups":[],"Name":"X","Tag":"","Transitions":[{"Type":"Transition","Actions":"","Description":"X => Quit State","DestinationState":"QUIT","Event":"","Guard":"return false;","metadata":{"points":[-79.05574035665609,422,-110.85233501419336,379.45762014483626,-140.71546967203,328.7909534781696,-167.20593801706116,270]}},{"Type":"Transition","Actions":"","Description":"X => Y","DestinationState":"Y","Event":"","Guard":"return User.Data(\"what?\").Boolean();","metadata":{"points":[-98,493.2126631967818,-162.63038379688572,489.948969447981,-224.63038379688572,479.61563611464766,-284,462.55377674236036]}}],"metadata":{"loc":"-23 497"}},{"Type":"State","Description":"temporary stop the work","Groups":[],"Name":"TEMP_STOP","Tag":"","Transitions":[{"Type":"Transition","Actions":"","Description":"TEMP_STOP to Z","DestinationState":"Z","Event":"","Guard":"return User.Event.EventName==\"SMS\"","metadata":{"points":[382,600.3539622462589,450.94460957923036,540.9273511292473,529.2779429125636,484.1611173630134,617,431.24330587997724]}}],"metadata":{"loc":"307 665"}},{"Type":"State","Description":"X should go here","Groups":[],"Name":"Y","Tag":"","Transitions":[],"metadata":{"loc":"-359 441"}},{"Type":"State","Description":"Z","Groups":[],"Name":"Z","Tag":"","Transitions":[{"Type":"Transition","Actions":"","Description":"Z to SLEEP","DestinationState":"SLEEP","Event":"","Guard":"return true","metadata":{"points":[694.1337011379732,461,696.4152141159578,541.195614232741,690.495317475131,620.195614232741,676.4953509369537,698]}},{"Type":"Transition","Actions":"","Description":"User timeout 1","DestinationState":"Z","Event":"","Guard":"return User.Event.EventName==\"TIMER1\"","metadata":{"points":[735.3012701892235,461,756.3012701892235,497.3730669589464,627.6987298107755,497.3730669589464,648.6987298107755,461]}}],"metadata":{"loc":"692 386"}},{"Type":"State","Description":"Sleep state!","Groups":[],"Name":"SLEEP","Tag":"","Transitions":[],"metadata":{"loc":"663 773"}},{"Type":"State","Description":"[new state]","Groups":[],"Name":"ZZZZ","Tag":"","Transitions":[],"metadata":{"loc":"745 101"}}]
public static FSM<TStateName, TEvent, TEventArgs> Deserialize(string jsonstring)
{
JsonSerializer<FSM<TStateName, TEvent, TEventArgs>> serializer =
new JsonSerializer<FSM<TStateName, TEvent, TEventArgs>>();
var model = serializer.DeserializeFromString(jsonstring);
return model;
}
public string Name { get; set; }
public string Description { get; set; }
public List<State> States
{
get => _states.Values.ToList();
set { _states = value.ToDictionary(s => s.Name, s => s); }
}
public TStateName FireEvent(TStateName fromstate, TEvent eventtype, TEventArgs args)
{
try
{
var currentstate = _states[fromstate];
if (currentstate == null)
{
UnknownEventHandler?.Invoke(fromstate, eventtype, args);
return fromstate;
}
var matchedtransition = currentstate?
.Transitions
.Where(e => e.Event.CompareTo(eventtype)==0)
.FirstOrDefault(t =>
String.IsNullOrWhiteSpace(t.Guard) || EvaluateGuard(fromstate, t.Guard, eventtype, args));
if (matchedtransition == null)
{
UnknownEventHandler?.Invoke(fromstate, eventtype, args);
return fromstate;
}
Transitioning?.Invoke(fromstate, matchedtransition.DestinationState, eventtype, args);
matchedtransition.Actions.ForEach(actiontext =>
ActionHandler?.Invoke(fromstate, matchedtransition.DestinationState, actiontext, eventtype, args));
return matchedtransition.DestinationState;
}
catch (Exception exception)
{
UnhandledExceptionHandler?.Invoke(exception, fromstate, eventtype, args);
return fromstate;
}
}
public Func<TStateName, string, TEvent, TEventArgs, bool> EvaluateGuard; // = (s, a, e, b) => throw new MissingFieldException("No guard handler assigned");
public Action<TStateName, TStateName, string, TEvent, TEventArgs> ActionHandler; // = (f, t, a, e, b) => throw new MissingFieldException("No action handler assigned");
public Action<TStateName, TStateName, TEvent, TEventArgs> Transitioning;
public Action<TStateName, TEvent, TEventArgs> UnknownEventHandler; // = (s, a, b) => throw new MissingFieldException("No unknown event handler assigned");
public Action<Exception, TStateName, TEvent, TEventArgs> UnhandledExceptionHandler; // = (e, s, ev, a) => throw new ApplicationException("No UnhandledExceptionHandler assigned", e);
private Dictionary<TStateName, State> _states;
public class State
{
public State(TStateName name, string description, List<Transition> transitions, string tag = null,
List<string> groups = null)
{
Tag = tag;
Groups = groups;
Name = name;
Description = description;
Transitions = transitions;
}
public string Tag { get; set; }
public List<string> Groups { get; set; }
public TStateName Name { get; set; }
public string Description { get; set; }
public List<Transition> Transitions { get; set; }
}
public class Transition
{
public Transition(TEvent tevent, string guard, TStateName destinationstate, List<string> actions,
string description)
{
Guard = guard;
Event = tevent;
Actions = actions;
Description = description;
DestinationState = destinationstate;
}
public TEvent Event { get; set; }
public string Guard { get; set; }
public TStateName DestinationState { get; set; }
public List<string> Actions { get; set; }
public string Description { get; set; }
}
}
} |
Thanks a lot for sharing. This is the kind of design I want to get into my state machine, splitting definition, execution, and state. |
I'm experiencing issues with the current implementation related to the storage of internal state:
The state machine should ideally be stateless, as the only state present within a state machine is the state itself, so it makes sense that the engine be assignable the current state, then an event fired, and the resultant state read back out. The IStateMachineSaver almost extracts that functionality, but is a bit messy, and would probably be better implemented as an external property accessor so the machine can acquire the current state itself when an event is fired, and obviously update the resultant state through the setter when a transition has occurred.
In situations where you might have many FSMs representing a collection of 'users' but all associated with the same FSM graph, you may well want to pass an event to lots of instances synchronously. The system appears to require you recreate the machine for each user, call
Load
with a newed-up IStateMachineSaver, then call Fire, then call Save to read the resultant state.To check an event for the next user, it appears we have to dispose of that machine, and restart the process from scratch.
I'm not sure if this is all related to the 'history' feature, which seems a bit superfluous given that we have logging, but I'd be interested to know if there are any workarounds for these limitations, other than rewriting the functionality in the source code.
As an example of the non-working code to implement a fast call on behalf of multiple instances:
Here you can see the messy use of the persistor, which of course doesn't work because Load can't be called twice. A shared FSM is a pretty common requirement, so am I missing something here?
The text was updated successfully, but these errors were encountered: