Acts As State Machine error handling

What is it?

Acts as state machine (AASM) is a rubygem for adding finite state machines to Ruby classes. http://github.com/rubyist/aasm. This is a widely used gem/plugin and is the de-facto standard for state management in Ruby on Rails.

Whilst working on the Technophobia Video Uploader (coming soon!!!) I hit a problem with aasm. I’ve used the gem before and think it can be overkill for certain situations but when used correctly has massive benefits.

How it works

The way acts as state machine works is by defining various states and event. The events have rules that determine which state transitions can take place and callbacks which allow functions to be executed at various stages of the transition. Take the code below for example:

 # State Machine
  aasm_column :status
  aasm_initial_state :pending

  aasm_state :pending
  aasm_state :encoded, :enter => :encode_video
  aasm_state :uploaded, :enter => :upload_video

  aasm_state :encode_failed
  aasm_state :upload_failed
  aasm_state :complete

  # State Machine Transitions
  aasm_event :encode do
    transitions :from => :pending, :to => :encoded
  end

  aasm_event :upload do
    transitions :from => :encoded, :to => :uploaded
  end

  aasm_event :complete do
    transitions :from => :uploaded, :to => :complete
  end

  aasm_event :failed do
    transitions :from => :pending, :to => :encode_failed
    transitions :from => :encoded, :to => :upload_failed
  end

This gives me 4 events that will cause the Objects state to move through 6 defined states. By calling Object.encode! I fire the encode event which will transition from the pending state to the encoded state going through any callbacks on the way. The callback method “encode_video” defined for the encode event is fired on entry into the transition. If the object state is anything other than pending I will get and InvalidStateTransition error as the encode! event should only allow pending objects to be processed.

The problem

The sequence which the callbacks and state change is called can be problematic, in the example above I use the enter callback to execute a method “encode_video”, this callback fires before the event status is changed.

    #callbacks and state change
    state.call_action(:before_enter, self)
    state.call_action(:enter, self) #our callback is executed
    self.aasm_current_state = state_name #the state is changed
    state.call_action(:after_enter, self)

This which works great unless you need to change state within the callback method, for example if the callback fails and you change the state to failed.
In my encode_video method I check to see if the process has been successful and if not I call the failed! event, this works and sets the status to the appropriate failure status. However when control is returned to the block of code above, the line “self.aasm_current_state = state_name” changes the state from my failure state to the state as defined in the transition.

The Solution

Fortunately there is a very simple solution do this. You can pass the :error option into the aasm_event method to set a callback that will be used when a StandardError is raised within one of the state callbacks. This will allow the program to keep running but will break out of the code at the correct point before the state is changed. So we change the state machine transitions like so:

  # State Machine Transitions
  aasm_event :encode, :error => :error_raised do
    transitions :from => :pending, :to => :encoded
  end

  aasm_event :upload, :error => :error_raised do
    transitions :from => :encoded, :to => :uploaded
  end

  aasm_event :complete, :error => :error_raised do
    transitions :from => :uploaded, :to => :complete
  end

Then define the error method:

  def error_raised(error)
    failed!
    logger.debug("Error raised in video_encoding #{error.message}")
  end

And finally raise the correct error class in the call back function:


  def encode_video
    begin
      encoder = VideoChimp::Encoder.new
      case encoding_profile.container
        when "mp4" then
          encoder.encode_mp4
        when "flv" then
          encoder.encode_flv
      end
      unless encoder.valid?
        raise StandardError.new("Something went wrong encoding")
      end
    rescue Exception => ex
      raise StandardError.new("Exception in encode_video #{ex.message}")
    end
  end

And thats it. More control over your state machine :D

Share and Enjoy:
  • Print
  • Digg
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • Blogplay
  • StumbleUpon
  • Twitter

About Danny

Primarily a Ruby On Rails developer, but I can do other cool stuff too.
This entry was posted in Development, Ruby / Rails and tagged , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>