Volatile memoization for Marshalled AR objects

The Ruby memcache-client gem uses the Marshal library to serialize ruby objects for storage in Memcached.

By default, all instance variables is marshalled as well, which pretty much defeats instance variable based memoization eg.

  def to_param; @to_param ||= "#{self.id}-#{self.name.to_url}" end 

Usage scenario

An account based application where almost all Models belongs to an account:

  class Account < ActiveRecord::Base
     acts_as_cached :version => 1, :include => [:subscription, :setting, :logo, :domain, :active_theme], :ttl => 1.day

     belongs_to :domain
     has_many :bookings, :class_name => 'PropertyBooking', :include => [:date, :customer], :foreign_key => :account_id, :dependent => :delete_all, :order => 'bookings.created_at DESC', :extend => BookingsExtension
    #define other relationships
  end

We set the account instance in a before filter for every request:

  class AccountController < ApplicationController
     attr_accessor :account 
     before_filter :account_from_subdomain

     protected

      def account_from_subdomain   
         @account = Account.get_cache([account_subdomain(),request.domain].join('.'))
         if @account.nil?
           account_not_found  
         else
          @account.disabled? ? account_disabled() : account_found()
        end    
     end         

  end

From time to time we need to reference the account belongs_to association to load/query other data scoped to the particular account:

  class Booking < ActiveRecord::Base
    #associations etc.

    #verify subscription limit before saving another booking
    def before_create; self.account.subscription.bookings_this_cycle? end 

  end

Headaches

Simply caching an account alongside a child Model forces us to implement excessive sweepers to preserve data integrity and also duplicates data, which isn't desireable:

class Booking < ActiveRecord::Base
  #cache account alongside this model  
   acts_as_cached :version => 1, :include => [:account, :date, :progress, :payment], :ttl => 2.hours

end

We can override the reader/accessor method, but this results in excessive Memcache lookups if the related model is referenced multiple times per request:

 class Booking < ActiveRecord::Base
  #do not cache the account alongside this model
  acts_as_cached :version => 1, :include => [:date, :progress, :payment], :ttl => 2.hours

  #potential multiple lookups
  def account; Account.get_cache(account_id) unless account_id.nil? end
end 

The solution

We can use the after_initialize callback to clear an instance variable used for memoization:

 class Booking < ActiveRecord::Base
  #do not cache the account alongside this model
  acts_as_cached :version => 1, :include => [:date, :progress, :payment], :ttl => 2.hours

  #memoization, only called once per lifecycle/request
  def account; @cached_account ||= Account.get_cache(account_id) unless account_id.nil? end

  #AR callback
  def after_initialize; @cached_account = nil end

end 

In theory either afterinitialize OR afterfind can be used to accomplish this.

You need to ensure your cache backend reference these callbacks to mimick and replicate the standard ActiveRecord behaviour.

I use the following snippet for cache_fu:

module ActsAsCached
   module ClassMethods

       def fetch_cache(id)
         return if ActsAsCached.config[:skip_gets]

           autoload_missing_constants do 
              data = cache_store(:get, cache_key(id))
              [:after_initialize, :after_find].each{|c| data.send(c) if data.respond_to? c }
              data
          end
      end
   end
end

memcache-client can be monkey patched if you rolled your own store using the CACHE.get API method:

class MemCache
  def get(key, raw = false)
    server, cache_key = request_setup key

    value = if @multithread then
              threadsafe_cache_get server, cache_key
            else
              cache_get server, cache_key
            end

    return nil if value.nil?

    unless raw
       value = Marshal.load value     
       [:after_initialize, :after_find].each{|c| value.send(c) if value.respond_to? c } 
    end

    return value
  rescue TypeError, SocketError, SystemCallError, IOError => err
    handle_error server, err
  end
end

Alternatives

The Marshal library define callbacks for more complex marshalling:

  class SomeModel < ActiveRecord::Base

     #prepare for serialization
     def marshal_dump
     end

     #do something when deserialized
     def marshal_load( serialized_string )
     end

  end

About this entry