0

Tussle of the State Machines

There’s no shortage of Ruby state machine libraries, but when we needed to implement a formal state machine we don’t find one which met all of our requirements.

I had a same problem in my project.What i needed was a polymorphic class that could have multiple number of state machines in it. Depending on the the relation the appropriate state machine should be used.

What i Wanted

call.rb
class Call
  include Mongoid::Document
  field :scheduled_at,          type: DateTime
  field :is_existing_customer,  type: Boolean
  field :note

  belongs_to :callable, polymorphic: true
  case callable_type
    when 'Car'
      state_machine :state, :initial => :fresh, namespace: 'car' do
        event :schedule do
          transition [:fresh, :schedule] => :scheduled
        end
        .....
        .....
      end
    when 'Personal'
       state_machine :state, :initial => :fresh, namespace: 'Personal' do
        event :schedule do
          transition :fresh => :scheduled
        end
        .....
        .....
      end
     when 'any other'
      .....
  end
end

The Main Problem

Since AASM State Machine does not supports multiple state machine in a single class. So i tried to achive it through state_machine gem with namespaces BUT AGAIN we can not have same states under namespaced state machine in a single class.

How to do it?

My basic requirement was to have a state machine that should be easily composable with other Ruby objects.So what i need to do was to define a state machine as a separate class and selectively apply itto our Rails models. since Mongodb supports embeded obects. I could use it to store states in it.

The Solution

We wanted a state machine that could be easily integrated with other Ruby objects. So we decided to define a state machine as a separate class and selectively apply it to our Rails models. We were using MongoDB, so we embedded these objects.

car_state_machine.rb
class CarStateMachine
  include Mongoid::Document

  field :state
  embedded_in :call

  # no need for name space and we can use AASM directly
  state_machine :state, :initial => :fresh do
    #states: fresh, scheduled, lead, succeed
    event :schedule do
      transition [:fresh, :schedule] => :scheduled
    end
    #...
    #...
  end
end
personal_state_machine.rb
class PersonalStateMachine
  include Mongoid::Document
  include AASM

  field :state
  embedded_in :call

  #states: hello, meet, bye
  state_machine :state, :initial => :hello do
    event :wow do
      transition :hello => :meet
    end
    #...
    #...
  end
end

My Call class now

So to call access these embedded objects i defined a method call_state that returns the embedded on the basis of the callable_type of Call Class.

call.rb
class Call
  include Mongoid::Document

  field :scheduled_at, type: DateTime
  field :is_existing_customer, type: Boolean
  field :note
  field :callable_type

  embeds_one :car_state_machine
  embeds_one :personal_state_machine

  # Method to access state machine
  def call_state
    case self.callable_type
    when 'Car'
      self.car_state_machine || self.build_car_state_machine
    when 'Personal'
      self.personal_state_machine || self.build_personal_state_machine
    end
  end
end

# Example
call = Call.first.callable_type # => "Car"
call.call_state.state # => 'fresh'
call.call_state.schedule!
call.call_state.state # => 'scheduled'

#####

call = Call.last.callable_type # => "Personal"
call.call_state.state # => 'hello'
call.call_state.wow!
call.call_state.state # => 'meet'
0

Rails Caching and Active Record Extensions

As a rails developer, I have gone through situations when we really want to get the most out of the limited resources. So we opt for digging out the areas where we can improve. And caching is one of the option everybody thinks about, and implement.

Situation

Lately I have been looking at the solution to cache the Active Record Objects directly. Also if it could cache the methods frequently used methods too. So I learn’t that Rails.cachecould actually store the Active records directly, that can be configured with redis easily.

The Problem

The code was too repetitive. Every Where I see was a similar pattern like

# Write
Rails.cache.write(key, value)
# Read
Rails.cache.read(key)
# Clear
Rails.cache.del(key)

This was becoming a real issue. So I thought if we can have class method where we could just pass in the method names, and create the cache, that could be shared with all models. So first solution I thought of was creating a Concern that would be included in all models. So came up with this soution.

The Solution

module CachedMethods
  extend ActiveSupport::Concern

  module ClassMethods
    def cached_methods(*methods)
      methods.each do |association|
        define_method("cached_#{association}") do |key = nil, cached = nil|
          key = "#{association}_for_#{self.class.to_s.underscore}_#{id}"
          cached = Rails.cache.read(key) rescue false
          return cached if cached
          load_assoc = send(association)
          Rails.cache.write(key, load_assoc, expires_in: 30.minutes)
          update_cache_keys(key)
          load_assoc
        end

        define_method("#{association}_is_cached?") do
          key = "#{association}_for_#{self.class.to_s.underscore}_#{id}"
          Rails.cache.read(key).present?
        end
      end
    end
  end

  private

  # keeps tracks of all cache keys, for the included class.
  define_method('update_cache_keys') do |key|
    multi_key = "#{self.class.to_s.underscore}_#{id}_cache_keys"
    keys = Rails.cache.read(multi_key) || []
    keys << key
    Rails.cache.write(multi_key, keys.uniq)
  end

  # can be called to clear all cached data for included class
  def delete_cache
    cached = Rails.cache.read("#{self.class.to_s.underscore}_#{id}_cache_keys")
    return unless cached
    cached.each { |key| Rails.cache.delete(key) }
  end
end

But I did not want this to manually including every where in the application. I don’t want to remember which concern to use. So thought of creating an extension for the Active record.

Improvement

So created an ActiveRecordExtension.

# Inculde custom enxtensions here
module ActiveRecordExtension
  extend ActiveSupport::Concern
  included do
    include CachedMethods
  end
end

# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)

Create an initializer called extensions, that will load it during application initialization.

require "active_record_extension"

How to Use

Now anybody can use this extension in the system easily.

# ========================================
#                  USAGE
# ========================================
# // Ruby.
Class Fruit
#   ...
#   # this code generates some instance methods
#   # cached_apples and cached_mangoes
#   # which when called the first time, will cache the ActiveRecord Object,
#   # and after every call will get the object back from the cache only.
#   # You can also cache the method results too.
#   # Also it creates cache check instance methods
#   # as in this case apples_is_cached? And mangos_is_cached?
cached_methods :apples, :mangos
#   # As the name suggests, this method will clear all the cache related
#   # to the class.
after_save :delete_cache
#   ...
end

Now that looks easier to use.

What is provides

fruit = Fruit.first
apples = fruit.cached_apples # cache apples
mangos = fruit.cached_mangoes # cache mangoes

# Now if you this code is run again
apples = fruit.cached_apples # from cache
mangoes = fruit.cached_mangoes # from cache

# the cache will auto expire after 30 minutes
# or if the Fruit object is updated.
# we have added delete_cache callback
after_save :delete_cache

How cool is that.