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








