State_machine gem

I’m working on a somewhat complex project that really begs for a state
machine to keep the logic all straight. I’ve been playing with the
state_machine [1] gem the last several days to see if it’s suitable. I
like the syntax and feature set, but I’m having difficulty mapping its
capabilities into code.

The main problem has to do with the relationship between state machines
and classes. While a class can have 1 (or more) state machines declared
inside of it, a state machine cannot be used across 1 (or more)
classes.

If I am correct about that limitation, I am wondering how to structure a
large and complex state machine. I like to keep my classes small and
simple so that they are easy to understand and debug. In the case where
a class has many states and events, the business logic attached to the
transitions (in my case) are rather complex. The size of the code in the
class very quickly balloons (a few hundred lines).

Here’s an example.

Assume I am building an application that connects to a web service. The
web service is a gateway to other services behind it, but it acts as a
broker for all requests. Upon connection to the web service gateway, the
gateway can indicate if it is available to forward requests,
delayed/behind in forwarding requests, or if the back-end services are
offline.

So, I build a machine that has several states along with unique behavior
for each one.

State - Starting
Initialize all libraries for connecting to the web service. Upon
completion, transition to the Connecting state.

State - Connecting
Open a network connection to the web service. Upon timeout, transition
to ConnectionError state. Upon successful connection, transition to the
Connected state.

State - Connected
Ask the web service for its current status. The web service can indicate
it is in 1 of 3 states itself. Okay, Delayed, or Offline. Depending upon
the response, transition to the appropriate state. In the meantime, each
of the Okay/Delayed/Offline states can get events indicating the web
service’s current status. It seems to me like this is appropriately
handled by the “Super state” Connected rather than duplicating this
functionality across multiple sub states.

As you can imagine from the example given above, jamming all of this
functionality into a single class would result in a pretty huge class
(hundreds or even thousands of lines).

I would like to offload some of that logic to other classes and have a
state transition “hand off” to another class while everything is still
controlled by the same overall machine. So, I guess what I want is a
machine to be able to encompass multiple classes.

Does anyone else have any experience with this gem and building machines
with more complex use-cases than the examples? Am I trying to shoe-horn
this gem into a use-case that it isn’t suited to solving? Should I
consider using a different statemachine gem? Suggestions?

cr

[1] GitHub - pluginaweek/state_machine: Adds support for creating state machines for attributes on any Ruby class

Chuck

Just a quick thought… I admit that I did not look too deep into
this… but in principle, you should be able to break a complex state
machine into a network of simpler ones. By “network” I mean that the
simpler state machines are interconnected by the fact the the “output”
of one machine is the “input” of another machine.

See if you can break down your model in a way that fits this paradigm.

BR,

Oren

Chuck R. wrote in post #1004925:

I’m working on a somewhat complex project that really begs for a state
machine to keep the logic all straight. I’ve been playing with the
state_machine [1] gem the last several days to see if it’s suitable. I
like the syntax and feature set, but I’m having difficulty mapping its
capabilities into code.

Does anyone else have any experience with this gem and building machines
with more complex use-cases than the examples? Am I trying to shoe-horn
this gem into a use-case that it isn’t suited to solving? Should I
consider using a different statemachine gem? Suggestions?

I’ve not got direct experience with the statemachine gem, but when I’ve
had complex state machines to write in the past, the approach which has
worked for me is to separate the state machine from the action code, and
just have the state machine send signals to a driver instance. You’ve
got to be a bit careful with making sure you can’t generate state
transitions from inside message handlers (for that way lies
stack-death), but otherwise I like the separation. It makes things nice
and testable.


Alex

On Jun 12, 2011, at 2:06 PM, Chuck R. wrote:

I’m working on a somewhat complex project that really begs for a state machine
to keep the logic all straight. I’ve been playing with the state_machine [1] gem
the last several days to see if it’s suitable. I like the syntax and feature set,
but I’m having difficulty mapping its capabilities into code.

The main problem has to do with the relationship between state machines and
classes. While a class can have 1 (or more) state machines declared inside of it,
a state machine cannot be used across 1 (or more) classes.

If I am correct about that limitation, I am wondering how to structure a large
and complex state machine. I like to keep my classes small and simple so that they
are easy to understand and debug. In the case where a class has many states and
events, the business logic attached to the transitions (in my case) are rather
complex. The size of the code in the class very quickly balloons (a few hundred
lines).

I figured out how to do this. I had to move from the state_machine gem
to the statemachine gem (note the lack of underscore in the second one).
It has support for a “class context” in which all state actions are
executed. It’s actually pretty easy to change the context while the
machine is running to get new or different behavior from another
business logic class.

Here’s a code example. This example also contains some code to show how
to register for events in one context class (as a closure) but still
have the event delivered to the correct context class if it changes.

require ‘rubygems’
require ‘statemachine’

class Registered
def on_event &blk
@block = blk
end

def call
@block.call
end
end

class Startup
attr_accessor :statemachine
attr_reader :block

def do_init()
register_events
statemachine.process_event(:started)
end

def change_context
fire
puts “change context for next state”
statemachine.context = Connected.new(statemachine,
@registered_events)
end

def register_events
@registered_events = Registered.new
@registered_events.on_event {
statemachine.process_event(:data_error) }
end

def fire
@registered_events.call
end

def on_error
puts “error in #{self.class}”
end
end

class Connected
attr_accessor :statemachine
attr_reader :block
def initialize sm, registered
@statemachine = sm
@registered_events = registered
@once = true
end

def initial_gw_logon
fire
puts “entered connected”

if @once
  puts "attempting GW logon"
  @once = false
end

end

def enable_order_modification
puts “enable order modification”
end

def enable_order_entry
puts “enable order entry”
end

def disable_order_entry
puts “disable order entry”
end

def disable_orders
puts “disable orders”
end

def fire
@registered_events.call
end

def on_error
puts “#{self.class}#on_error
end
end # Connected

connection_machine = Statemachine.build do
startstate :operational

superstate :operational do
# we don’t set a context within this builder block, so using
:starting
# as the startstate causes an error; it immediately transitions to
:starting
# and tries to run #on_init which doesn’t exist anyplace yet (no
context)
startstate :waiting

trans :waiting, :startup, :starting
event :data_error, :error_mode

state :starting do
  on_entry :do_init

  event :started, :connected, :change_context
end

superstate :connected do
  startstate :gateway_down
  on_entry :initial_gw_logon

  state :gateway_down do
    event :dc_down, :gateway_down
    event :dc_up, :gateway_down
    event :gw_down, :gateway_down
    event :gw_up, :gateway_up
  end

  state :gateway_up do
    on_entry :enable_order_modification
    on_exit :disable_orders

    event :dc_down, :gateway_up, :disable_order_entry
    event :dc_up, :gateway_up, :enable_order_entry
    event :gw_up, :gateway_up
    event :gw_down, :gateway_down
  end

end # superstate connected

end # superstate operational

state :error_mode do
on_entry :on_error
event :operate, :operational_H, Proc.new { puts “exiting error
mode”}
event :gw_up, :error_mode
end

end

startup = Startup.new
connection_machine.context = startup
connection_machine.context.statemachine = connection_machine

after using the business logic in Startup, it switches the machine

over to using

the business logic in the Connected class

connection_machine.process_event(:startup)

connection_machine.process_event(:gw_up)

This produces:

Event: startup
exiting ‘waiting’ state
entering ‘starting’ state
Event: started
exiting ‘starting’ state
Event: data_error
exiting ‘starting’ state
exiting ‘operational’ superstate
entering ‘error_mode’ state
error in Startup
change context for next state
entering ‘connected’ superstate
Event: data_error
exiting ‘gateway_down’ state
exiting ‘connected’ superstate
exiting ‘operational’ superstate
entering ‘error_mode’ state
Connected#on_error
entered connected
attempting GW logon
entering ‘gateway_down’ state
Event: gw_up