Class: EventState::Machine
- Inherits:
-
EventMachine::Connection
- Object
- EventMachine::Connection
- EventState::Machine
- Defined in:
- lib/event_state/machine.rb
Overview
Base class for state machines driven by EventMachine
.
This class provides a domain-specific language (DSL) for declaring the structure of the machine. See the README for examples and the general idea of how this works.
If you are sending ruby objects as messages, see ObjectMachine; it handles serialization (using EventMachine’s ObjectProtocol
) and names messages according to their classes (but you can override this).
If you have some other kind of messages, then you should subclass this class directly. Two methods are required:
-
Override EventMachine’s
receive_data
method to call #transition_on_recv with the received message. -
Override EventMachine’s
send_data
method to call #transition_on_send with the message to be sent.
Note that #transition_on_recv and #transition_on_send take a message name as well as a message. You have to define the mapping from messages to message names so that the message names correspond with the transitions declared using the DSL (Machine.on_send and Machine.on_recv in particular).
Direct Known Subclasses
Class Attribute Summary collapse
-
.start_state ⇒ State
readonly
The machine enters this state when a new connection is established.
-
.states ⇒ Hash<Symbol, State>
readonly
Map from state names (ruby symbols) to States.
Instance Attribute Summary collapse
-
#state ⇒ State
readonly
The machine’s current state.
Class Method Summary collapse
-
.on_enter(*message_names) {|message| ... } ⇒ nil
Register a block to be called after the machine enters the current Machine.state.
-
.on_exit(*message_names) {|message| ... } ⇒ nil
Register a block to be called after the machine exits the current Machine.state.
-
.on_recv(*args) ⇒ nil
Declare which state the machine transitions to when one of the given messages is received in this Machine.state.
-
.on_send(*args) ⇒ nil
Declare which state the machine transitions to when one of the given messages is sent in this Machine.state.
-
.on_unbind { ... } ⇒ nil
Called when EventMachine calls
unbind
on the connection while it is in the current state. -
.print_state_machine_dot(opts = {}) ⇒ IO
Print the state machine in dot (graphviz) format.
-
.protocol { ... } ⇒ nil
Declare the protocol; pass a block to declare the Machine.states.
-
.reverse_protocol(base) { ... } ⇒ nil
Exchange sends for receives and receives for sends in the
base
protocol, and clear all of the Machine.on_enter and Machine.on_exit handlers. -
.state(state_name) { ... } ⇒ nil
Declare a state; pass a block to configure the state using Machine.on_enter, Machine.on_send and so on.
-
.timeout(timeout) { ... } ⇒ nil
Set maximum time (in seconds) that the machine should spend in this state.
-
.transitions ⇒ Array
The complete list of transitions declared in the state machine (an edge list).
Instance Method Summary collapse
-
#add_state_timer(timeout) { ... } ⇒ Integer
Add a timer that will be fired only while the machine is in the current state; the timer is cancelled when the machine leaves the current state (either by sending or receiving a message, or when unbind is called).
-
#post_init ⇒ nil
Called by
EventMachine
when a new connection has been established. -
#transition_on_recv(message_name, message) ⇒ nil
Move the state machine from its current state to the successor state that it should be in after receiving the given
message
, according to the protocol defined using the DSL. -
#transition_on_send(message_name, message) {|message| ... } ⇒ nil
Move the state machine from its current state to the successor state that it should be in after sending the given
message
, according to the protocol defined using the DSL. -
#unbind ⇒ nil
Called by
EventMachine
when a connection is closed.
Class Attribute Details
.start_state ⇒ State (readonly)
Returns the machine enters this state when a new connection is established.
292 293 294 |
# File 'lib/event_state/machine.rb', line 292 def start_state @start_state end |
Instance Attribute Details
#state ⇒ State (readonly)
Returns the machine’s current state.
375 376 377 |
# File 'lib/event_state/machine.rb', line 375 def state @state end |
Class Method Details
.on_enter(*message_names) {|message| ... } ⇒ nil
Register a block to be called after the machine enters the current state.
The machine changes state in response to a message being sent or received, and you can register an on_enter handler that is specific to the message that caused the change. Or you can render a catch-all block that will be called if no more specific handler was found (see example below).
If a catch-all on_enter
block is given for the start_state, it is called from EventMachine’s post_init
method. In this case (and only this case), the message passed to the block is nil
.
158 159 160 161 162 163 164 165 166 167 |
# File 'lib/event_state/machine.rb', line 158 def on_enter *, &block raise "on_enter must be called from a state block" unless @state if == [] raise "on_enter block already given" if @state.default_on_enter @state.default_on_enter = block else save_state_handlers('on_enter', @state.on_enters, ,block) end nil end |
.on_exit(*message_names) {|message| ... } ⇒ nil
181 182 183 184 185 186 187 188 189 190 |
# File 'lib/event_state/machine.rb', line 181 def on_exit *, &block raise "on_exit must be called from a state block" unless @state if == [] raise "on_exit block already given" if @state.default_on_exit @state.default_on_exit = block else save_state_handlers('on_exit', @state.on_exits, , block) end nil end |
.on_recv(*args) ⇒ nil
246 247 248 249 250 251 252 253 |
# File 'lib/event_state/machine.rb', line 246 def on_recv *args next_state_name = args.pop = args raise "on_recv must be called from a state block" unless @state .flatten.each do |name| @state.on_recvs[name] = next_state_name end end |
.on_send(*args) ⇒ nil
Declare which state the machine transitions to when one of the given messages is sent in this state.
211 212 213 214 215 216 217 218 |
# File 'lib/event_state/machine.rb', line 211 def on_send *args next_state_name = args.pop = args raise "on_send must be called from a state block" unless @state .flatten.each do |name| @state.on_sends[name] = next_state_name end end |
.on_unbind { ... } ⇒ nil
Called when EventMachine calls unbind
on the connection while it is in the current state. This may indicate that the connection has been closed or timed out. The default is to take no action.
229 230 231 232 233 |
# File 'lib/event_state/machine.rb', line 229 def on_unbind &block raise "on_unbind must be called from a state block" unless @state @state.on_unbind = block nil end |
.print_state_machine_dot(opts = {}) ⇒ IO
Print the state machine in dot (graphviz) format.
By default, the ‘send’ edges are red and ‘receive’ edges are blue, and the start state is indicated by a double border.
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
# File 'lib/event_state/machine.rb', line 330 def print_state_machine_dot opts={} io = opts[:io] || StringIO.new = opts[:graph_options] || '' = opts[:message_name_transform] || proc {|x| x} state_name_transform = opts[:state_name_transform] || proc {|x| x} recv_edge_style = opts[:recv_edge_style] || 'color=blue' send_edge_style = opts[:send_edge_style] || 'color=red' io.puts "digraph #{self.name.inspect} {\n #{}" io.puts " #{start_state.name} [peripheries=2];" # double border transitions.each do |state_name, (kind, ), next_state_name| s0 = state_name_transform.call(state_name) s1 = state_name_transform.call(next_state_name) label = .call() style = case kind when :recv then "#{recv_edge_style},label=\"#{label}\"" when :send then "#{send_edge_style},label=\"#{label}\"" else raise "unknown kind #{kind}" end io.puts " #{s0} -> #{s1} [#{style}];" end io.puts "}" io end |
.protocol { ... } ⇒ nil
Declare the protocol; pass a block to declare the states.
When the block terminates, this method declares any ‘implicit’ states that have been referenced by on_send or on_recv but that have not been declared with state. It also does some basic sanity checking.
There can be multiple protocol blocks declared for one class; it is equivalent to moving all of the definitions to the same block.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/event_state/machine.rb', line 40 def protocol raise "cannot nest protocol blocks" if defined?(@protocol) && @protocol @start_state = nil unless defined?(@start_state) @states = {} unless defined?(@states) @protocol = true @state = nil yield @protocol = false # add implicitly defined states to @states to avoid having to check for # nil states while the machine is running explicit_states = Set[*@states.keys] all_states = Set[*@states.values.map {|state| state.on_sends.values + state.on_recvs.values}.flatten] implicit_states = all_states - explicit_states implicit_states.each do |state_name| @states[state_name] = State.new(state_name) end end |
.reverse_protocol(base) { ... } ⇒ nil
Exchange sends for receives and receives for sends in the base
protocol, and clear all of the on_enter and on_exit handlers. It often happens that a server and a client follow protocols with the same states, but with sends and receives interchanged. This method is intended to help with this case. You can, for example, reverse a server and use the passed block to define new on_enter and on_exit handlers appropriate for the client.
The start state is determined by the first state declared in the given block (not by the protocol being reversed).
80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/event_state/machine.rb', line 80 def reverse_protocol base, &block raise "cannot mirror if already have a protocol" if defined?(@protocol) @states = Hash[base.states.map {|state_name, state| new_state = EventState::State.new(state_name) new_state.on_recvs = state.on_sends.dup new_state.on_sends = state.on_recvs.dup [state_name, new_state] }] protocol(&block) end |
.state(state_name) { ... } ⇒ nil
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/event_state/machine.rb', line 104 def state state_name raise "must be called from within a protocol block" unless @protocol raise "cannot nest calls to state" if @state # create new state or edit exiting state @state = @states[state_name] || State.new(state_name) # configure this state yield # need to know the start state @start_state = @state unless @start_state # index by name for easy lookup @states[@state.name] = @state # ensure that on_enter etc. aren't callable outside of a state block @state = nil end |
.timeout(timeout) { ... } ⇒ nil
Set maximum time (in seconds) that the machine should spend in this state. By default, there is no limit. If you set a timeout without passing a block, the default action is to call close_connection
.
Note that the timeout block will not be executed if unbind is called in this state. However, if the timeout block calls close_connection, the state’s on_unbind handler will be called.
Note that the timeout applies to all machines. If you want to set a timeout ‘at run time’ that applies to the machine only while it is in a particular state, see #add_state_timer.
Internally, this uses EventMachine’s add_timer
method. It is independent of EventMachine’s comm_inactivity_timeout
.
275 276 277 278 279 280 |
# File 'lib/event_state/machine.rb', line 275 def timeout timeout, &block raise "on_recv must be called from a state block" unless @state @state.timeout = timeout @state.on_timeout = block if block_given? nil end |
.transitions ⇒ Array
The complete list of transitions declared in the state machine (an edge list).
301 302 303 304 305 306 |
# File 'lib/event_state/machine.rb', line 301 def transitions states.values.map{|state| [[state.on_sends, :send], [state.on_recvs, :recv]].map {|pairs, kind| pairs.map{|, next_state_name| [state.name, [kind, ], next_state_name]}}}.flatten(2) end |
Instance Method Details
#add_state_timer(timeout) { ... } ⇒ Integer
Add a timer that will be fired only while the machine is in the current state; the timer is cancelled when the machine leaves the current state (either by sending or receiving a message, or when unbind is called).
Internally, this uses EventMachine’s add_timer
method.
507 508 509 510 511 512 513 |
# File 'lib/event_state/machine.rb', line 507 def add_state_timer timeout, &handler sig = EM.add_timer(timeout) do self.instance_exec(&handler) end @state_timer_sigs << sig sig end |
#post_init ⇒ nil
Called by EventMachine
when a new connection has been established. This calls the on_enter
handler for the machine’s start state with a nil
message.
Be sure to call super
if you override this method, or the on_enter
handler for the start state will not be called.
387 388 389 390 391 392 393 |
# File 'lib/event_state/machine.rb', line 387 def post_init @state = self.class.start_state @state_timer_sigs = [] add_state_timer @state.timeout, &@state.on_timeout if @state.timeout @state.call_on_enter self, nil, nil nil end |
#transition_on_recv(message_name, message) ⇒ nil
Move the state machine from its current state to the successor state that it should be in after receiving the given message
, according to the protocol defined using the DSL.
If the received message is not valid according to the protocol, then the protocol error handler is called (see ProtocolError).
The precise order of events is:
-
the
on_exit
handler of the current state is called withmessage
-
the current state is set to the successor state
-
the
on_enter
handler of the new current state is called withmessage
432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
# File 'lib/event_state/machine.rb', line 432 def transition_on_recv , #puts "#{self.class}: RECV #{message_name}" # look up successor state next_state_name = @state.on_recvs[] # if there is no registered successor state, it's a protocol error if next_state_name.nil? raise RecvProtocolError.new(self, @state.name, , ) else transition , , next_state_name end nil end |
#transition_on_send(message_name, message) {|message| ... } ⇒ nil
Move the state machine from its current state to the successor state that it should be in after sending the given message
, according to the protocol defined using the DSL.
If the message to be sent is not valid according to the protocol, then the protocol error handler is called (see ProtocolError). If the message is valid, then the precise order of events is:
-
this method yields the message to the supplied block (if any); the intention is that the block is used to actually send the message
-
the
on_exit
handler of the current state is called withmessage
-
the current state is set to the successor state
-
the
on_enter
handler of the new current state is called withmessage
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 |
# File 'lib/event_state/machine.rb', line 476 def transition_on_send , #puts "#{self.class}: SEND #{message_name}" # look up successor state next_state_name = @state.on_sends[] # if there is no registered successor state, it's a protocol error if next_state_name.nil? raise SendProtocolError.new(self, @state.name, , ) else # let the caller send the message before we transition yield if block_given? transition , , next_state_name end nil end |
#unbind ⇒ nil
Called by EventMachine
when a connection is closed. This calls the on_unbind handler for the current state and then cancels all state timers.
402 403 404 405 406 407 |
# File 'lib/event_state/machine.rb', line 402 def unbind #puts "#{self.class} UNBIND" handler = @state.on_unbind self.instance_exec(&handler) if handler cancel_state_timers end |