Understanding Statemachines, Part 4: Superstates 6

Posted by Micah Sat, 07 Apr 2007 21:19:00 GMT

Superstates

Often in statemachines, duplication can arise. For example, the vending machine in our examples may need periodic repairs. It’s not certain which state the vending machine will be in when the repair man arrives. So all states should have a transition into the Repair Mode state.


Diagram 1 - Without Superstates

In this diagram, both the Waiting and Paid states have a transition to the Repair Mode invoked by the repair event. Duplication! We can dry this up by using the Superstate construct. See below:


Diagram 2 - With Superstates

Here we introduce the Operational superstate. Both the Waiting and Paid states are contained within the superstate which implies that they inherit all of the superstate’s transitions. That means we only need one transition into the Repair Mode state from the Operational superstate to achieve the same behavior as the solution in diagram 1.

One statemachine may have multiple superstates. And every superstate may contain other superstates. ie. Superstates can be nested.

History State

The solution in diagram 2 has an advantage over diagram 1. In diagram 1, once the repair man is done he triggers the operate event and the vending machine transitions into the Waiting event. This is unfortunate. Even if the vending machine was in the Paid state before the repair man came along, it will be in the Waiting state after he leaves. Shouldn’t it go back into the Paid state?

Superstates come with the history state which solves this problem. Every superstate will remember which state it is in before the superstate is exited. This memory is stored in a pseudo state called the history state. Transitions that end in the history state will recall the last active state of the superstate and enter it.

You can see the history state being use in diagram 2. In this solution, the history state allows the vending machine to return from a repair session into the same state it was in before, as though nothing happened at all.

Code

The following code builds the statemachine in diagram 2. Watch out for the _H. This is how the history state is denoted. If you have a superstate named foo, then it’s history state will be named foo_H.

require 'rubygems'
require 'statemachine'

vending_machine = Statemachine.build do
  superstate :operational do
    trans :waiting, :dollar, :paid
    trans :paid, :selection, :waiting
    trans :waiting, :selection, :waiting
    trans :paid, :dollar, :paid

    event :repair, :repair_mode,  Proc.new { puts "Entering Repair Mode" }
  end

  trans :repair_mode, :operate, :operational_H, Proc.new { puts "Exiting Repair Mode" }

  on_entry_of :waiting, Proc.new { puts "Entering Waiting State" }
  on_entry_of :paid, Proc.new { puts "Entering Paid State" }
end

vending_machine.repair
vending_machine.operate
vending_machine.dollar
vending_machine.repair
vending_machine.operate

Output:

Entering Repair Mode
Exiting Repair Mode
Entering Waiting State
Entering Paid State
Entering Repair Mode
Exiting Repair Mode
Entering Paid State

Next we should cover pseudo states.

Storing Binary Data in Postgres? Beware! 2

Posted by Micah Sun, 18 Feb 2007 06:05:00 GMT

If your situation matches the following conditions, beware!

  1. You’re working in rails.
  2. You’re using Postgresql.
  3. You’re storing binary data in the database.

This was the situation on a project of mine. We were storing PDFs and PNG images in our Postgrs database. Everything was fine during development and testing where we used files that ranged up to a few dozen kilobytes in size. The situation went rapidly downhill when file sizes got up in the hundreds of kilobytes to megabytes. The worst part about it was that the errors we got were misleading and seemingly random. They included:

  • undefined method `<<' for nil:NilClass
  • invalid end of buffer
  • undefined class/module Packet (Packet being a model class which was definitely defined)

These errors sent us on a wile goose chase. The clue that finally pointed us toward the problem was the fact that it took 6 seconds to load a 4M PDF document from the database. That was far too long especially considering the same document could be loaded from a file instantaneously.

Apparently, binary data stored in Postgresql’s bytea data type has to be parsed on save and load to escape and unescape certain characters. Unfortunately the native C postgres gem doesn’t do the parsing. It’s done in the PostgreSQLAdapter.unescape_bytea and PostgreSQLAdapter.escape_bytea methods (Ruby code) of ActiveRecord and the parsing is a bit too intensive for Ruby. This is where the meltdown begins. It consumes too much memory, or too much processing power, or … well I don’t know exactly. But I know it breaks.

We refactored our model such that all the binary data gets stored in flat files on disk rather than in the database. After this, our Rails app came back to life. It was MUCH faster too!

Here’s hoping that if and when you encounter this problem, Google points you to this blog entry and you find it helpful.

Understanding Statemachines, Part 3: Conditional Logic 1

Posted by Micah Tue, 13 Feb 2007 03:34:00 GMT

Conditions

If you’re doing any significant amount of work with statmachines, you will most certainly encounter some conditional logic in your statemachines. Take our vending machine. When ever a coin is inserted, the invoked event will depend on whether the total amount of money inserted is sufficient to buy something. If enough money has been tendered, the display should suggest that the customer make a selection. If insufficient money has been inserted, the customer should be prompted to insert more.

Conditional logic can be accomplished by using entry actions. See the diagram below.


State Diagram with Conditional Logic

Starting in the Accept Money state, when a coin is inserted, the coin event is fired and the statemachine transitions into the Coin Inserted state. This is where it gets fun. Upon entering of the Coin Inserted state its entry event is invoked: count_amount_tendered. This method will count the money and invoke the not_paid_yet or paid event accordingly. This will cause the statemachine to transition into the appropriate state.

The Coin Inserted state is unique. You wouldn’t expect to find the statemachine in the Coin Inserted state for any reason except to make this decision. Once the decision is made, the state changes. States like this are called Decision States.

Code

require 'rubygems'
require 'statemachine'

class VendingMachineContext

  attr_accessor :statemachine

  def initialize
    @amount_tendered = 0
  end

  def add_coin
    @amount_tendered = @amount_tendered + 25
  end

  def count_amount_tendered
    if @amount_tendered >= 100
      @statemachine.paid
    else
      @statemachine.not_paid_yet
    end
  end

  def prompt_money
    puts "$.#{@amount_tendered}: more money please"
  end

  def prompt_selection
    puts "please make a selection"
  end
end

vending_machine = Statemachine.build do
  trans :accept_money, :coin, :coin_inserted, :add_coin
  state :coin_inserted do
    event :not_paid_yet, :accept_money, :prompt_money
    event :paid, :await_selection, :prompt_selection
    on_entry :count_amount_tendered
  end
  context VendingMachineContext.new
end
vending_machine.context.statemachine = vending_machine

vending_machine.coin
vending_machine.coin
vending_machine.coin
vending_machine.coin

Output:

$.25: more money please
$.50: more money please
$.75: more money please
please make a selection

Next lesson: Superstates
Understanding Statemachines, Part 4: Superstates

Older posts: 1 2 3 4 5 ... 9