dotnet-state-machine / stateless

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

State machine definition vs state machine instance - reusing instances #232

Open tystol opened 6 years ago

tystol commented 6 years ago

I may be falling into the premature optimisation trap, but I have a requirement to process bulk actions, that may result in 10's of thousands of individual triggers on individual state machines (web app, so will be loading the state of each one from db and re-instantiating the state machines per request).

So it got me thinking, why is the definition of the state machine tied to the state? If it wasn't, I could have a single static instance of the state machine definition, and only load the state, rather than running all the Configure methods 10,000 times. Something like this:

var phoneCallMachine = new StateMachine<State, Trigger>();

phoneCallMachine.Configure(State.OffHook)
    .Permit(Trigger.CallDialled, State.Ringing);

phoneCallMachine.Configure(State.Ringing)
    .Permit(Trigger.CallConnected, State.Connected);

// ...

// using explicit typed var just for demo purposes:
StateMachineInstance<State, Trigger> phoneCall = phoneCallMachine.Create(State.OffHook);
phoneCall.Fire(Trigger.CallDialled);
Assert.AreEqual(State.Ringing, phoneCall.State);

Or is it simply that the perf gain of not re-running the Configure methods negligible?

scottctr commented 6 years ago

I had a similar scenario and created NStateManager to address. In my case, having the state manager coupled to a single instance of a managed object also led to having to instantiate a few other classes per request. Moving to a single instance of these led to a significant performance improvement. Take a look and let me know what you think.

tangkhaiphuong commented 6 years ago

The main problem belongs to the design of Stateless machine don't support pass context of state(Accessor/Mutator)/callback(OnEntry/OnExit/OnActivated/OnExit) so when configure state machine coupled with the object inside.

I think need some overload method inside Stateless can support pass object so can help resolve this case.

tangkhaiphuong commented 6 years ago

@scottctr @tystol I looked deep into the design of Stateless C# and found it's only supported for instance and not for configuration as you request. The solution from @scottctr is suitable for this case. I think there are 2 approaches to support separate between configuration & instance.

  1. Refactor & redesign StateMachine class support for define state for Type like @scottctr suggestion. Can't implement overload base current design due to some internal variable can't share with multi-instance for this case.
  2. Keep current design, create 2 news class call: StateMachineManager<TState, TTrigger, TContext> & StateConfigurationManager<TState, TTrigger, TContext> to support pre-defined flow & state transition for TContext. For create state machine via method StateMachineManager<TState, TTriger, TContext>.Create(TContext context);
tangkhaiphuong commented 6 years ago

@nblumhardt @scottctr @tystol I have implemented support separate State machine definition vs state machine instance in typescript version. https://github.com/tangkhaiphuong/stateless/tree/master/src/context

Basically, the new approach can apply for Stateless C# without breaking original design by implement overload generic class with support take TContext for instance. Mean: StateMachine<TState, TTrigger> then we implement new class: StateMachine<TState, TTrigger, TContext>

And same for remain class to support both modes: state machine host embedded to object and separate state machine configuration then create context.

I implement in my fork branch for Stateless C# to support this and pull to request review and merge this feature.

patrious commented 5 years ago

@tangkhaiphuong Was this pull request ever put into Stateless?

tangkhaiphuong commented 5 years ago

@patrious Not yet implement. I only implement base on my approach for TypeScript version. I wait from the more discuss from others for any new ideal.

gregory-seidman commented 3 years ago

I like this idea for a number of reasons:

  1. While there may be a use case I can't think of, certainly the common case is that no configuration is ever performed after the first time Fire() (or FireAsync() or whatever) is called.
  2. This follows the builder pattern that is common across lots of .NET libraries, especially those from Microsoft.
  3. If implemented properly (i.e. without copying), this can substantially improve performance when identical state machines are being created over and over for short-lived objects, e.g. in a web service that handles the workflow of some object.

I prefer a builder design to passing a context, but one can be wrapped to produce the other.

HenningNT commented 3 years ago

I agree, this is a feature we should implement, and it's quite high up on the things-I'd-Like-To-Do list.

ThomasEg commented 3 years ago

Like OP i'm running into this issue with thousands of instances per-request and was wondering if there is a consensus on how to approach this issue?

@tystol did you solve your challenge and if so, which route did you take? And was it successfull or just another fist full of angry killerbees?

josellm commented 2 years ago

I have created a gist with a workaround to use a single state machine per configuration https://gist.github.com/josellm/95b04a3e1c45a4c0fb01a0186865a109