-
-
Notifications
You must be signed in to change notification settings - Fork 272
Finite State Machines
There are many options for describing Finite State Machines (FSM) with Celluloid.
Celluloid provides its own Celluloid::FSM
module which is loosely modeled after Erlang's gen_fsm. While relatively sparse on features (which some might consider a good thing), this module is directly integrated with Celluloid's features like timers, making it easy to describe FSMs which automatically transition to another state after a timeout.
class Connection
include Celluloid::FSM
default_state :connected
state :connected, :to => :disconnected do
puts "Connected!"
end
state :disconnected
end
The statemachine gem has been around for several years and is quite mature. Its primary difference from the state_machine gem is the ability to swap "context" references so that you can have different behavior for the same events depending upon the current state.
There is no out-of-the-box support for Celluloid in the statemachine gem and thankfully none is necessary. The statemachine itself should not be an actor. To prove that, let's run a simple example.
require 'statemachine'
class VendingMachineContext
def activate
puts "activating"
end
def release(product)
puts "releasing product: #{product}"
end
def refund
puts "refunding dollar"
end
def sales_mode
puts "going into sales mode"
end
def operation_mode
puts "going into operation mode"
end
def on_dollar
@statemachine.dollar
end
def on_selection(*args)
@statemachine.selection(*args)
end
end
class Machine
class VendingStateMachine < Statemachine::Statemachine
end
def self.build
Statemachine.build(VendingStateMachine.new) do
state :waiting do
event :dollar, :paid, :activate
event :selection, :waiting
on_entry :sales_mode
on_exit :operation_mode
end
trans :paid, :selection, :waiting, :release
trans :paid, :dollar, :paid, :refund
context VendingMachineContext.new
end
end
end
vending_machine = Machine.build
vending_machine.dollar
vending_machine.dollar
vending_machine.selection "Peanuts"
# going into sales mode
# going into operation mode
# activating
# refunding dollar
# releasing product: Peanuts
# going into sales mode
As shown, the statemachine transitions through its states properly and executes the appropriate entry, exit and guard actions for each state.
Issuing several asynchronous events to the machine results in very peculiar behavior.
require 'statemachine'
require 'celluloid'
class VendingMachineContext
def activate
puts "activating"
end
def release(product)
puts "releasing product: #{product}"
end
def refund
puts "refunding dollar"
end
def sales_mode
puts "going into sales mode"
end
def operation_mode
puts "going into operation mode"
end
def on_dollar
@statemachine.dollar
end
def on_selection(*args)
@statemachine.selection(*args)
end
end
class Machine
class VendingStateMachine < Statemachine::Statemachine
include Celluloid
end
def self.build
Statemachine.build(VendingStateMachine.new) do
state :waiting do
event :dollar, :paid, :activate
event :selection, :waiting
on_entry :sales_mode
on_exit :operation_mode
end
trans :paid, :selection, :waiting, :release
trans :paid, :dollar, :paid, :refund
context VendingMachineContext.new
end
end
end
vending_machine = Machine.build
vending_machine.async.dollar
vending_machine.async.dollar
vending_machine.async.selection "Peanuts"
# going into sales mode
# going into operation mode
# going into operation mode
# activating
# activating
# D, [2013-02-08T15:11:04.270277 #88091] DEBUG -- : Terminating 5 actors...
# D, [2013-02-08T15:11:04.278727 #88091] DEBUG -- : Shutdown completed cleanly
By default, Celluloid actors run in ATOM mode (see Pipelining and execution modes and Glossary for more information). As a result, a single actor can process multiple messages in parallel. In the example above, the dollar
event is sent to the machine which in turn calls the action :activate
. Since the machine itself is an actor, the call to :activate
results in the machine being suspended (it's a message to an actor) so the second dollar
event begins processing. Since the machine contains mutable state (that is, the State that the machine is in can change depending upon the events it has received) then ATOM mode is unsafe.
It's tempting to include the exclusive
method in the class definition. This indicates that the entire object should process messages one at a time; it will not process any further messages until the current message has completed. Unfortunately, the statemachine must call into itself to transition between various states, execute actions, etc. As a result, it hangs almost immediately.
The solution is to invert the delivery of events to the state machine.
require 'statemachine'
require 'celluloid'
class VendingMachineContext
include Celluloid
#6
exclusive
# 2
def initialize
@statemachine = Machine.build
@statemachine.context = wrapped_object
@statemachine.initialized
end
def activate
puts "activating"
end
def release(product)
puts "releasing product: #{product}"
end
def refund
puts "refunding dollar"
end
def sales_mode
puts "going into sales mode"
end
def operation_mode
puts "going into operation mode"
end
# 4
def on_dollar
@statemachine.dollar
end
# 5
def on_selection(*args)
@statemachine.selection(*args)
end
end
class Machine
class VendingStateMachine < Statemachine::Statemachine
end
def self.build
Statemachine.build(VendingStateMachine.new) do
# 3
trans :initializing, :initialized, :waiting
state :waiting do
event :dollar, :paid, :activate
event :selection, :waiting
on_entry :sales_mode
on_exit :operation_mode
end
trans :paid, :selection, :waiting, :release
trans :paid, :dollar, :paid, :refund
#1
end
end
end
vending_machine = VendingMachineContext.new
vending_machine.async.on_dollar
vending_machine.async.on_dollar
vending_machine.async.on_selection "Peanuts"
# going into sales mode
# going into operation mode
# activating
# refunding dollar
# releasing product: Peanuts
# going into sales mode
# D, [2013-02-08T15:11:04.270277 #88091] DEBUG -- : Terminating 5 actors...
# D, [2013-02-08T15:11:04.278727 #88091] DEBUG -- : Shutdown completed cleanly
Let's step through the changes that were made to make the statemachine safe for use within an actor.
We need to get to a point where all events are being sent to an actor's mailbox so that access is serialized. We also need to allow the statemachine to execute actions and modify its state without getting suspended (thereby allowing a race condition on state mutation) or blocked (by making multiple calls into an actor and deadlocking).
We do this by inverting the relationship between state machine and context that we had in our earlier examples. Instead of instantiating the machine, which in turn instantiates the context, we flip it.
-
Remove instantiation of the context from the machine creation.
-
Add an
initialize
method to the VendingMachineContext class. Give it the responsibility for instantiating the state machine. The change in #2 causes a small problem. When callingMachine.build
the machine wants to go into the default state of:waiting
. It makes this transition before any context is set, so it blows up when trying to execute the on_entry action ofsales_mode
. To resolve this, we need to add a new initial state to the machine. -
Add an
:initializing
state. Send the machine the:initialized
event in VendingMachineContext#initialize after the context has been assigned. Also, note that the context is assigned thewrapped_object
instead ofActor.current
. As mentioned above, we want the state machine to be able to transition between states and execute any actions without getting suspended. Therefore, it needs to work on the bare object and not the actor. -
Add the
on_dollar
method as a way to send thedollar
event to the machine. As a method on the Actor, these messages will come through the actor's mailbox. -
Add the
on_selection
method. Reasoning is the same as for #4. -
As a consequence of exposing the
wrapped_object
in step 3, we need to make access to the actor exclusive. This is also known as Erlang-mode.
Always feel free to:
- Visit the
#celluloid
channel on freenode. - Post a bug report or feature request.
- Ask questions on our mailing list.